Add migration-17-25 module: Java 17 class files run on 17 and 25, flags, removed APIs, jdeps and jdeprscan

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Gth99spB1CzQmBFofXmU4Z
This commit is contained in:
Claude
2026-09-28 08:42:19 +00:00
parent 814c32b5ce
commit 57a04dc393
20 changed files with 304 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
# migration-17-25
Companion module for the ankurm.com post *Java 17 to Java 25 Migration Guide: The Direct Jump*.
The probes in `src/probes/` are compiled **once** with JDK 17 (`--release 17`) and run unchanged on JDK 17.0.20.1 and JDK 25.0.4.1, which is what a team skipping 21 actually does. `src/removed/` holds four tiny sources that use APIs removed or newly deprecated between 17 and 25; they are compiled with both JDKs.
```bash
export JDK17=/path/to/jdk-17 JDK25=/path/to/jdk-25
./scripts/run-all.sh # regenerates output/
```
| Output | Shows |
|---|---|
| `01-runtime-behaviour.txt` | Security Manager, `Thread.stop`, default charset, `finalize`, `Unsafe` on both runtimes |
| `02-startup-flags.txt` | Which 17-era JVM options still start on 25 |
| `03-compile-removed-apis.txt` | What `javac` says about four APIs on each JDK |
| `04-jdeps-jdeprscan.txt` | The scanners run with JDK 25 tools against the JDK 17-built jar |
| `05-gc-defaults.txt` | Default collector and heap ergonomics with no flags |
| `06-legacy-stack-on-25.txt` | `mvn test` on JDK 25 for four generations of one Spring app (from `spring-boot-demo/openrewrite`) |
| `07-framework-minimums.txt` | Vendor documentation quotes (not produced by running code) |
@@ -0,0 +1,33 @@
# 01-runtime-behaviour: classes compiled once with JDK 17 (--release 17), run unchanged on 17 and 25; LC_ALL=C
--- SecurityManagerProbe
[17.0.20.1] WARNING: A terminally deprecated method in java.lang.System has been called
[17.0.20.1] WARNING: System::setSecurityManager has been called by SecurityManagerProbe (file:/tmp/migration-17-25-work/c17/)
[17.0.20.1] WARNING: Please consider reporting this to the maintainers of SecurityManagerProbe
[17.0.20.1] WARNING: System::setSecurityManager will be removed in a future release
[17.0.20.1] installed: true
[25.0.4.1] failed: java.lang.UnsupportedOperationException: Setting a Security Manager is not supported
--- ThreadStopProbe
[17.0.20.1] stop() accepted; thread alive afterwards: false
[25.0.4.1] stop() failed: java.lang.UnsupportedOperationException
--- CharsetProbe
[17.0.20.1] Charset.defaultCharset() = US-ASCII
[17.0.20.1] bytes written for the 4-letter word with an accent = 4 (UTF-8 would be 5)
[25.0.4.1] Charset.defaultCharset() = UTF-8
[25.0.4.1] bytes written for the 4-letter word with an accent = 5 (UTF-8 would be 5)
--- FinalizerProbe
[17.0.20.1] finalize() ran: true
[25.0.4.1] finalize() ran: true
--- UnsafeProbe
[17.0.20.1] read back: 42
[25.0.4.1] WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
[25.0.4.1] WARNING: sun.misc.Unsafe::allocateMemory has been called by UnsafeProbe (file:/tmp/migration-17-25-work/c17/)
[25.0.4.1] WARNING: Please consider reporting this to the maintainers of class UnsafeProbe
[25.0.4.1] WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
[25.0.4.1] read back: 42
--- SecurityManagerProbe with -Djava.security.manager=allow
[17.0.20.1] WARNING: A terminally deprecated method in java.lang.System has been called
[17.0.20.1] WARNING: System::setSecurityManager has been called by SecurityManagerProbe (file:/tmp/migration-17-25-work/c17/)
[17.0.20.1] WARNING: Please consider reporting this to the maintainers of SecurityManagerProbe
[25.0.4.1] Error occurred during initialization of VM
[25.0.4.1] java.lang.Error: A command line option has attempted to allow or enable the Security Manager. Enabling a Security Manager is not supported.
[25.0.4.1] at java.lang.System.initPhase3([email protected]/System.java:1970)
@@ -0,0 +1,11 @@
# flag | JDK 17 | JDK 25 (exit code: first warning or error line)
-Djava.security.manager=allow | exit 0: (no message) | exit 1: Error occurred during initialization of VM
-XX:+UseBiasedLocking | exit 0: Option UseBiasedLocking was deprecated in version 15.0 and will likely be removed in a future release. | exit 1: Unrecognized VM option 'UseBiasedLocking'
-XX:+UseConcMarkSweepGC | exit 1: Unrecognized VM option 'UseConcMarkSweepGC' | exit 1: Unrecognized VM option 'UseConcMarkSweepGC'
-XX:+AggressiveOpts | exit 1: Unrecognized VM option 'AggressiveOpts' | exit 1: Unrecognized VM option 'AggressiveOpts'
-XX:MaxPermSize=128m | exit 1: Unrecognized VM option 'MaxPermSize=128m' | exit 1: Unrecognized VM option 'MaxPermSize=128m'
-XX:+UseParallelOldGC | exit 1: Unrecognized VM option 'UseParallelOldGC' | exit 1: Unrecognized VM option 'UseParallelOldGC'
-Xverify:none | exit 0: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release. | exit 0: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
-XX:-UseCompressedClassPointers | exit 0: (no message) | exit 0: Option UseCompressedClassPointers was deprecated in version 25.0 and will likely be removed in a future release.
-XX:+ParallelRefProcEnabled | exit 0: (no message) | exit 0: (no message)
-XX:+UseStringDeduplication | exit 0: (no message) | exit 0: (no message)
@@ -0,0 +1,9 @@
# 03-compile-removed-apis: javac -Xlint:removal on JDK 17 and JDK 25
UsesCompiler.java [17.0.20.1] [removal] Compiler in java.lang has been deprecated and marked for removal
UsesCompiler.java [25.0.4.1] cannot find symbol
UsesRunFinalization.java [17.0.20.1] compiles cleanly
UsesRunFinalization.java [25.0.4.1] [removal] runFinalization() in Runtime has been deprecated and marked for removal
UsesThreadGroupDestroy.java [17.0.20.1] [removal] destroy() in ThreadGroup has been deprecated and marked for removal
UsesThreadGroupDestroy.java [25.0.4.1] [removal] destroy() in ThreadGroup has been deprecated and marked for removal
UsesThreadSuspend.java [17.0.20.1] [removal] suspend() in Thread has been deprecated and marked for removal
UsesThreadSuspend.java [25.0.4.1] cannot find symbol
@@ -0,0 +1,16 @@
# 04-jdeps-jdeprscan (scanning probes.jar, built by JDK 17, with the JDK 25 tools)
--- jdeps --jdk-internals
probes.jar -> jdk.unsupported
UnsafeProbe -> sun.misc.Unsafe JDK internal API (jdk.unsupported)
--- jdeprscan --release 17 --for-removal
Jar file probes.jar:
class SecurityManagerProbe uses deprecated class java/lang/SecurityManager (forRemoval=true)
class SecurityManagerProbe uses deprecated method java/lang/System::setSecurityManager(Ljava/lang/SecurityManager;)V (forRemoval=true)
class SecurityManagerProbe uses deprecated method java/lang/System::getSecurityManager()Ljava/lang/SecurityManager; (forRemoval=true)
--- jdeprscan --release 25 --for-removal
Jar file probes.jar:
class FinalizerProbe$Res overrides deprecated method java/lang/Object::finalize()V (forRemoval=true)
class SecurityManagerProbe uses deprecated class java/lang/SecurityManager (forRemoval=true)
class SecurityManagerProbe uses deprecated method java/lang/System::setSecurityManager(Ljava/lang/SecurityManager;)V (forRemoval=true)
class SecurityManagerProbe uses deprecated method java/lang/System::getSecurityManager()Ljava/lang/SecurityManager; (forRemoval=true)
class ThreadStopProbe uses deprecated method java/lang/Thread::stop()V (forRemoval=true)
+17
View File
@@ -0,0 +1,17 @@
# 05-gc-defaults (no flags, same 2-CPU machine)
--- JDK 17.0.20.1
InitialHeapSize = 98566144
MaxHeapSize = 1570766848
UseG1GC = true
collector: G1 Young Generation
collector: G1 Old Generation
processors: 2
--- JDK 25.0.4.1
InitialHeapSize = 98566144
MaxHeapSize = 1570766848
UseCompactObjectHeaders = false
UseG1GC = true
collector: G1 Young Generation
collector: G1 Concurrent GC
collector: G1 Old Generation
processors: 2
@@ -0,0 +1,5 @@
# 06-legacy-stack-on-25: mvn test on JDK 25 for four generations of the same app (bytecode target is release 17 in every row)
Spring Boot 2.7.18 (Spring 5.3, Hibernate 5.6): Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS
Spring Boot 3.5.16: Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS
Spring Boot 4.0.8: Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS
Spring Boot 4.1.1: Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS
@@ -0,0 +1,6 @@
# 07-framework-minimums (quoted from vendor documentation on 2026-09-28; NOT produced by running code)
Spring Boot 4.1.1 system requirements (https://docs.spring.io/spring-boot/system-requirements.html):
"Spring Boot 4.1.1 requires at least Java 17 and is compatible with versions up to and including Java 26."
Spring Framework 7.0.9 or above; Maven 3.6.3 or later; Gradle 8.14 or later, and 9.x
Gradle Java compatibility (https://docs.gradle.org/current/userguide/compatibility.html), "support for running Gradle":
Java 21: 8.5 and after | Java 24: 8.14 and after | Java 25: 9.1.0 and after | Java 26: 9.4.0 and after
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Which JVM options from Java 17-era startup scripts still work? Runs `java <flag> -version` on both JDKs; records exit code and first warning/error line.
J17=${JDK17:-/tmp/tools/j17}; J25=${JDK25:-/tmp/tools/jdk-25.0.4.1+1}
echo "# flag | JDK 17 | JDK 25 (exit code: first warning or error line)"
for f in -Djava.security.manager=allow -XX:+UseBiasedLocking -XX:+UseConcMarkSweepGC -XX:+AggressiveOpts -XX:MaxPermSize=128m -XX:+UseParallelOldGC -Xverify:none -XX:-UseCompressedClassPointers -XX:+ParallelRefProcEnabled -XX:+UseStringDeduplication; do
line="$f |"
for j in "$J17" "$J25"; do
all=$("$j/bin/java" $f -version 2>&1); rc=$?
msg=$(echo "$all" | grep -E 'Unrecognized|warning|Error' | head -1 | sed 's/^OpenJDK 64-Bit Server VM warning: //' | cut -c1-120)
[ -z "$msg" ] && msg="(no message)"
line="$line exit $rc: $msg |"
done
echo "${line% |}"
done
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Regenerates output/. Needs JDK 17 and JDK 25 (set JDK17 / JDK25), and, for 06, the openrewrite module's legacy app from spring-boot-demo (LEGACY_APP).
# The probes are compiled ONCE with JDK 17 (--release 17) and run unchanged on both runtimes: that is the "direct jump" scenario.
set -uo pipefail
cd "$(dirname "$0")/.."
J17=${JDK17:-/tmp/tools/j17}; J25=${JDK25:-/tmp/tools/jdk-25.0.4.1+1}; OUT=$PWD/output; C=/tmp/migration-17-25-work; rm -rf "$C"; mkdir -p "$C" "$OUT"
export JDK17=$J17 JDK25=$J25
"$J17/bin/javac" -encoding UTF-8 --release 17 -Xlint:-options -nowarn -d "$C/c17" src/probes/*.java 2>/dev/null
q() { grep -v -E '^Picked up'; }
{ echo "# 01-runtime-behaviour: classes compiled once with JDK 17 (--release 17), run unchanged on 17 and 25; LC_ALL=C"
for p in SecurityManagerProbe ThreadStopProbe CharsetProbe FinalizerProbe UnsafeProbe; do
echo "--- $p"
for j in "$J17" "$J25"; do
v=$("$j/bin/java" -version 2>&1 | q | head -1 | sed -E 's/^openjdk version "([^"]+)".*/\1/')
LC_ALL=C "$j/bin/java" -cp "$C/c17" $p 2>&1 | q | sed "s/^/ [$v] /"
done
done
echo "--- SecurityManagerProbe with -Djava.security.manager=allow"
for j in "$J17" "$J25"; do
v=$("$j/bin/java" -version 2>&1 | q | head -1 | sed -E 's/^openjdk version "([^"]+)".*/\1/')
LC_ALL=C "$j/bin/java" -Djava.security.manager=allow -cp "$C/c17" SecurityManagerProbe 2>&1 | q | head -3 | sed "s/^/ [$v] /"
done
} > "$OUT/01-runtime-behaviour.txt"
./scripts/flags.sh > "$OUT/02-startup-flags.txt" 2>&1
{ echo "# 03-compile-removed-apis: javac -Xlint:removal on JDK 17 and JDK 25"
for f in src/removed/*.java; do
for j in "$J17" "$J25"; do
v=$("$j/bin/java" -version 2>&1 | q | head -1 | sed -E 's/^openjdk version "([^"]+)".*/\1/')
r=$("$j/bin/javac" -encoding UTF-8 -Xlint:removal -d "$C/x" "$f" 2>&1 | q | grep -E 'error:|warning:' | head -1 | sed -E 's/^[^ ]+ (error|warning): //')
printf '%-28s [%s] %s\n' "$(basename "$f")" "$v" "${r:-compiles cleanly}"
done
done
} > "$OUT/03-compile-removed-apis.txt"
"$J17/bin/jar" cf "$C/probes.jar" -C "$C/c17" .
{ echo "# 04-jdeps-jdeprscan (scanning probes.jar, built by JDK 17, with the JDK 25 tools)"
echo "--- jdeps --jdk-internals"; "$J25/bin/jdeps" --jdk-internals "$C/probes.jar" 2>&1 | q | sed -n '1,2p'
echo "--- jdeprscan --release 17 --for-removal"; "$J25/bin/jdeprscan" --release 17 --for-removal "$C/probes.jar" 2>&1 | q | sed 's/^Jar file .*/Jar file probes.jar:/'
echo "--- jdeprscan --release 25 --for-removal"; "$J25/bin/jdeprscan" --release 25 --for-removal "$C/probes.jar" 2>&1 | q | sed 's/^Jar file .*/Jar file probes.jar:/'
} > "$OUT/04-jdeps-jdeprscan.txt"
{ echo "# 05-gc-defaults (no flags, same 2-CPU machine)"
for j in "$J17" "$J25"; do
v=$("$j/bin/java" -version 2>&1 | q | head -1 | sed -E 's/^openjdk version "([^"]+)".*/\1/')
echo "--- JDK $v"
"$j/bin/java" -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E ' (UseG1GC|MaxHeapSize|InitialHeapSize|UseCompactObjectHeaders) ' | awk '{print " " $2, $3, $4}'
"$j/bin/java" -cp "$C/c17" DefaultsProbe 2>&1 | q | sed 's/^/ /'
done
} > "$OUT/05-gc-defaults.txt"
# The four apps come from spring-boot-demo/openrewrite: legacy-app is checked in, step1..step3 are produced by its scripts/run-all.sh under $OR_WORK.
LEG=${LEGACY_APP:-/tmp/spring-boot-demo/openrewrite/legacy-app}; ORW=${OR_WORK:-/tmp/or-work}
{ echo "# 06-legacy-stack-on-25: mvn test on JDK 25 for four generations of the same app (bytecode target is release 17 in every row)"
for row in "Spring Boot 2.7.18 (Spring 5.3, Hibernate 5.6)|$LEG" "Spring Boot 3.5.16|$ORW/step1" "Spring Boot 4.0.8|$ORW/step2" "Spring Boot 4.1.1|$ORW/step3"; do
name=${row%%|*}; dir=${row#*|}
if [ -d "$dir" ]; then
rm -rf "$C/app"; cp -r "$dir" "$C/app"; rm -rf "$C/app/target"
r=$(cd "$C/app" && JAVA_HOME=$J25 PATH=$J25/bin:$PATH mvn -B test 2>&1 | grep -E 'Tests run:.*Fail|BUILD' | grep -v 'Time elapsed' | sed 's/\[INFO\] //' | tr '\n' ' ')
printf '%-52s %s\n' "$name:" "$r"
else printf '%-52s %s\n' "$name:" "(not found: $dir)"; fi
done
} > "$OUT/06-legacy-stack-on-25.txt"
cat > "$OUT/07-framework-minimums.txt" <<'EOT'
# 07-framework-minimums (quoted from vendor documentation on 2026-09-28; NOT produced by running code)
Spring Boot 4.1.1 system requirements (https://docs.spring.io/spring-boot/system-requirements.html):
"Spring Boot 4.1.1 requires at least Java 17 and is compatible with versions up to and including Java 26."
Spring Framework 7.0.9 or above; Maven 3.6.3 or later; Gradle 8.14 or later, and 9.x
Gradle Java compatibility (https://docs.gradle.org/current/userguide/compatibility.html), "support for running Gradle":
Java 21: 8.5 and after | Java 24: 8.14 and after | Java 25: 9.1.0 and after | Java 26: 9.4.0 and after
EOT
ls -1 "$OUT"; rm -rf "$C"
@@ -0,0 +1,15 @@
import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.*;
/** Writes the word cafe with an accented e with the platform default charset and reads the bytes back. Default is UTF-8 from Java 18 (JEP 400). */
public class CharsetProbe {
public static void main(String[] args) throws Exception {
Path f = Files.createTempFile("charset", ".txt");
try (Writer w = new FileWriter(f.toFile())) { w.write("caf\u00e9"); }
byte[] bytes = Files.readAllBytes(f);
System.out.println("Charset.defaultCharset() = " + Charset.defaultCharset());
System.out.println("bytes written for the 4-letter word with an accent = " + bytes.length + " (UTF-8 would be 5)");
Files.delete(f);
}
}
@@ -0,0 +1,9 @@
import java.lang.management.ManagementFactory;
/** Prints which garbage collector the JVM chose with no flags at all. */
public class DefaultsProbe {
public static void main(String[] args) {
ManagementFactory.getGarbageCollectorMXBeans().forEach(b -> System.out.println("collector: " + b.getName()));
System.out.println("processors: " + Runtime.getRuntime().availableProcessors());
}
}
@@ -0,0 +1,12 @@
/** finalize() is deprecated for removal since 18. Does it still run on 25? */
public class FinalizerProbe {
static volatile boolean finalized;
@SuppressWarnings({"removal", "deprecation"})
static class Res { @Override protected void finalize() { finalized = true; } }
public static void main(String[] args) throws Exception {
new Res();
for (int i = 0; i < 20 && !finalized; i++) { System.gc(); Thread.sleep(50); }
System.out.println("finalize() ran: " + finalized);
}
}
@@ -0,0 +1,12 @@
/** Java 17: installing a SecurityManager works (deprecated for removal). Java 24+: it cannot be installed at all. */
public class SecurityManagerProbe {
@SuppressWarnings({"removal", "deprecation"})
public static void main(String[] args) {
try {
System.setSecurityManager(new SecurityManager());
System.out.println("installed: " + (System.getSecurityManager() != null));
} catch (Throwable t) {
System.out.println("failed: " + t.getClass().getName() + ": " + t.getMessage());
}
}
}
@@ -0,0 +1,16 @@
/** Thread.stop() worked (deprecated) on 17. From Java 20 it throws UnsupportedOperationException. */
public class ThreadStopProbe {
@SuppressWarnings({"removal", "deprecation"})
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> { try { Thread.sleep(5000); } catch (InterruptedException e) { } });
t.start();
try {
t.stop();
t.join(1000);
System.out.println("stop() accepted; thread alive afterwards: " + t.isAlive());
} catch (Throwable e) {
System.out.println("stop() failed: " + e.getClass().getName());
t.interrupt();
}
}
}
@@ -0,0 +1,14 @@
import java.lang.reflect.Field;
/** sun.misc.Unsafe memory access: JEP 471 deprecates it for removal (23), JEP 498 warns at first use (24). */
public class UnsafeProbe {
public static void main(String[] args) throws Exception {
Field f = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
f.setAccessible(true);
sun.misc.Unsafe u = (sun.misc.Unsafe) f.get(null);
long addr = u.allocateMemory(8);
u.putLong(addr, 42L);
System.out.println("read back: " + u.getLong(addr));
u.freeMemory(addr);
}
}
@@ -0,0 +1,5 @@
public class UsesCompiler {
public static void main(String[] args) {
java.lang.Compiler.disable();
}
}
@@ -0,0 +1,5 @@
public class UsesRunFinalization {
public static void main(String[] args) throws Exception {
Runtime.getRuntime().runFinalization();
}
}
@@ -0,0 +1,5 @@
public class UsesThreadGroupDestroy {
public static void main(String[] args) {
new ThreadGroup("g").destroy();
}
}
@@ -0,0 +1,6 @@
public class UsesThreadSuspend {
@SuppressWarnings("deprecation")
public static void main(String[] args) {
Thread.currentThread().suspend();
}
}