JDK 21 和 JDK 25 是两个相邻的 LTS,中间隔着 22、23、24 三个非 LTS 版本。多数 LTS 到 LTS 的升级都是”安静”的:你的代码大概率原样能跑。 但 21 到 25 这两年是”预览转正 + 移除清理”集中发生的窗口,有几类坑是编译器和 JVM 不会替你兜底的:预览 API 改了形状、遗留线程 API 被删、sun.misc.Unsafe 开始打印警告、Security Manager 彻底关死、虚拟线程在 synchronized 里的行为被重写。 这篇文章是一份可执行的迁移清单:每条结论都在本机的 JDK 21(21.0.8)和 JDK 25(25+37-LTS)上分别编译或运行过,原始输出贴在对应的 ```txt 块里。凡是没在本机验证过的,都单独标注”未实测”。
实验环境与取证方式 本机是 Apple M1 Pro(10 核 arm64,macOS),装了 JDK 21.0.8 和 JDK 25 两个版本,路径如下:
1 2 3 4 5 6 7 8 9 $ /Library/Java/JavaVirtualMachines/jdk-21.jdk/Contents/Home/bin/java -version java version "21.0.8" 2025-07-15 LTS Java(TM) SE Runtime Environment (build 21.0.8+12-LTS-250) Java HotSpot(TM) 64-Bit Server VM (build 21.0.8+12-LTS-250, mixed mode, sharing) $ /Library/Java/JavaVirtualMachines/jdk-25.jdk/Contents/Home/bin/java -version java version "25" 2025-09-16 LTS Java(TM) SE Runtime Environment (build 25+37-LTS-3491) Java HotSpot(TM) 64-Bit Server VM (build 25+37-LTS-3491, mixed mode, sharing)
全文只用 JDK 自带的 java / javac / jar / jdeps / jdeprscan / jcmd / jfr,没有安装任何额外软件;压测程序都是自己写的。验证手法统一为两条:
同一份源码 分别丢给两个 javac 编译,比较报错原文;
同一个 jar / 同一个程序 分别用两个 java 运行,比较输出或耗时。
所有实验都在 /tmp/jdk-migration 下进行:源码放 src/,编译产物按版本分到 b21/(JDK 21)与 b25/(JDK 25),预览编译产物在 b21p/、b25p/。下面命令里的 -cp b21、-cp b25 就是这个意思;少数输出里带 file:/private/tmp/jdk-migration/b25/ 的绝对路径,也是真实打印出来的。
本机 JDK 的输出是中文 locale(zh_CN),所以下面编译错误是中文的;同一份 JDK 在英文 locale 下会打印内容相同、措辞为英文的信息,不影响判断。
语言与 API:从预览到正式 这一节每个特性都给一份源码,然后对照它在两个 JDK 上的命运。核心规律是:预览特性不会长期停留在”预览”,要么转正、要么被删除、要么以改过的形状继续预览 ——三种情况在这两年里都出现了(Scoped Values 转正、Gatherers 在 21 上压根不存在、Structured Concurrency 改形状后仍预览),所以凡是依赖 --enable-preview 的代码都要逐个过一遍。
未命名变量 _:JDK 21 预览,JDK 22 转正 未命名变量与未命名模式由 JEP 443 (JDK 21 预览)提出、JEP 456 (JDK 22 正式)定稿,两者都用下划线 _ 表示”这个位置必须声明但我不使用它”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.util.List;import java.util.concurrent.atomic.AtomicInteger;public class Unnamed { public static void main (String[] args) { AtomicInteger hits = new AtomicInteger (); List<String> items = List.of("alpha" , "beta" , "gamma" ); items.forEach(_ -> hits.incrementAndGet()); try { Integer.parseInt("not-a-number" ); } catch (NumberFormatException _) { hits.incrementAndGet(); } int loops = 0 ; for (String _ : items) { loops++; } System.out.println("hits=" + hits.get() + ", loops=" + loops); } }
JDK 21 上,不加 --enable-preview 直接编译,第一条 _ 就被拦下:
1 2 3 4 5 6 $ jdk-21/bin/javac -d b21 src/Unnamed.java src/Unnamed.java:10: 错误: 未命名变量 是预览功能,默认情况下禁用。 items.forEach(_ -> hits.incrementAndGet()); ^ (请使用 --enable-preview 以启用 未命名变量) 1 个错误
JDK 21 加 --enable-preview 能编译运行;JDK 25 上什么都不用加,直接通过:
1 2 3 4 5 6 7 8 9 $ jdk-21/bin/javac --release 21 --enable-preview -d b21p src/Unnamed.java 注: src/Unnamed.java 使用 Java SE 21 的预览功能。 注: 有关详细信息,请使用 -Xlint:preview 重新编译。 $ jdk-21/bin/java --enable-preview -cp b21p Unnamed hits=4, loops=3 $ jdk-25/bin/javac -d b25 src/Unnamed.java $ jdk-25/bin/java -cp b25 Unnamed hits=4, loops=3
升级动作:删掉 --enable-preview 即可 。如果你的构建里还带着这个开关,25 上它会变成纯粹的噪音。
Stream Gatherers:JDK 21 上根本不存在 Stream.gather(...) 和 java.util.stream.Gatherers 由 JEP 461 (JDK 22 预览)提出,JEP 473 (JDK 23 二次预览)打磨,JEP 485 (JDK 24)定稿。注意它的起点是 JDK 22——JDK 21 连预览都没有 ,这是与 _ 不同的地方:在 21 上加 --enable-preview 也救不回来。
1 2 3 4 5 6 7 8 9 10 11 12 13 import java.util.List;import java.util.stream.Gatherers;import java.util.stream.Stream;public class GathererDemo { public static void main (String[] args) { List<List<Integer>> windows = Stream.of(1 , 2 , 3 , 4 , 5 , 6 , 7 ) .gather(Gatherers.windowFixed(3 )) .toList(); System.out.println("windows=" + windows); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ jdk-21/bin/javac --release 21 --enable-preview -d b21p src/GathererDemo.java src/GathererDemo.java:2: 错误: 找不到符号 import java.util.stream.Gatherers; ^ 符号: 类 Gatherers 位置: 程序包 java.util.stream src/GathererDemo.java:9: 错误: 找不到符号 .gather(Gatherers.windowFixed(3)) ^ 符号: 变量 Gatherers 位置: 类 GathererDemo 2 个错误 $ jdk-25/bin/javac -d b25 src/GathererDemo.java $ jdk-25/bin/java -cp b25 GathererDemo windows=[[1, 2, 3], [4, 5, 6], [7]]
升级动作:这类代码在 21 上根本编译不过,所以不存在”从 21 升到 25 后坏掉”的风险;它是升到 25 之后新增可用 的能力。如果要让同一份源码兼容 21 和 25,Gatherers 的对应逻辑得自己在 21 上实现(例如用 Stream.iterator 或手写收集器),没有别的办法。
Scoped Values:JDK 21 预览,JDK 25 转正 Scoped Values 由 JEP 446 (JDK 21 预览)提出,历经 JEP 464 (22)、JEP 481 (23)、JEP 487 (24)多次预览,最终由 JEP 506 (JDK 25)定稿——官网头部的 Status: Closed / Delivered、Release: 25 可查。
1 2 3 4 5 6 7 8 9 public class Scoped { private static final ScopedValue<String> USER = ScopedValue.newInstance(); public static void main (String[] args) { ScopedValue.where(USER, "alice" ).run(() -> System.out.println("user=" + USER.get())); System.out.println("stillBound=" + USER.isBound()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 $ jdk-21/bin/javac -d b21 src/Scoped.java src/Scoped.java:3: 错误: ScopedValue 是预览 API,默认情况下处于禁用状态。 private static final ScopedValue<String> USER = ScopedValue.newInstance(); ^ (请使用 --enable-preview 以启用预览 API) src/Scoped.java:3: 错误: ScopedValue 是预览 API,默认情况下处于禁用状态。 private static final ScopedValue<String> USER = ScopedValue.newInstance(); ^ (请使用 --enable-preview 以启用预览 API) src/Scoped.java:6: 错误: ScopedValue 是预览 API,默认情况下处于禁用状态。 ScopedValue.where(USER, "alice").run(() -> System.out.println("user=" + USER.get())); ^ (请使用 --enable-preview 以启用预览 API) 3 个错误 $ jdk-21/bin/javac --release 21 --enable-preview -d b21p src/Scoped.java 注: src/Scoped.java 使用 Java SE 21 的预览功能。 注: 有关详细信息,请使用 -Xlint:preview 重新编译。 $ jdk-21/bin/java --enable-preview -cp b21p Scoped user=alice stillBound=false $ jdk-25/bin/javac -d b25 src/Scoped.java $ jdk-25/bin/java -cp b25 Scoped user=alice stillBound=false
升级动作:和 _ 一样,删掉 --enable-preview 即可。JEP 506 相对 JDK 24 预览只改了一处:ScopedValue.orElse 不再接受 null 参数。如果你在 21/24 上用了 orElse(null) 这类写法,25 上要换成显式的空值处理。
Foreign Function & Memory API:JDK 21 预览,JDK 22 转正 FFM API(java.lang.foreign)由 JEP 442 (JDK 21 预览)提出,JEP 454 (JDK 22)定稿。用同一个调 getpid 的程序看两版差异:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.lang.foreign.Arena;import java.lang.foreign.FunctionDescriptor;import java.lang.foreign.Linker;import java.lang.foreign.MemorySegment;import java.lang.foreign.SymbolLookup;import java.lang.foreign.ValueLayout;import java.lang.invoke.MethodHandle;public class FfmCall { public static void main (String[] args) throws Throwable { Linker linker = Linker.nativeLinker(); try (Arena arena = Arena.ofConfined()) { SymbolLookup libc = linker.defaultLookup(); MemorySegment getpid = libc.find("getpid" ).orElseThrow(); MethodHandle handle = linker.downcallHandle(getpid, FunctionDescriptor.of(ValueLayout.JAVA_INT)); System.out.println("getpid=" + (int ) handle.invokeExact()); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 $ jdk-21/bin/javac -d b21 src/FfmCall.java src/FfmCall.java:1: 错误: Arena 是预览 API,默认情况下处于禁用状态。 import java.lang.foreign.Arena; ^ (请使用 --enable-preview 以启用预览 API) [中间省略:每个 java.lang.foreign 的导入与使用点各报一次] src/FfmCall.java:16: 错误: FunctionDescriptor 是预览 API,默认情况下处于禁用状态。 MethodHandle handle = linker.downcallHandle(getpid, FunctionDescriptor.of(ValueLayout.JAVA_INT)); ^ (请使用 --enable-preview 以启用预览 API) 14 个错误 $ jdk-21/bin/java --enable-preview -cp b21p FfmCall WARNING: A restricted method in java.lang.foreign.Linker has been called WARNING: java.lang.foreign.Linker::downcallHandle has been called by the unnamed module WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for this module getpid=75464 $ jdk-25/bin/java -cp b25 FfmCall WARNING: A restricted method in java.lang.foreign.Linker has been called WARNING: java.lang.foreign.Linker::downcallHandle has been called by FfmCall in an unnamed module (file:/private/tmp/jdk-migration/b25/) WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module WARNING: Restricted methods will be blocked in a future release unless native access is enabled getpid=75442
这里同时暴露了两个层面的差异:
预览转正 :21 上 Linker 是预览 API,不加 --enable-preview 编不过;25 上直接编译运行。
受限方法警告在升级后被加强 (JEP 472 ,JDK 24 的”Integrity by Default”准备步骤):两版都会在首次调用 downcallHandle 时打印警告,但 25 多了一行 Restricted methods will be blocked in a future release unless native access is enabled,并标出了具体的调用类名。
升级动作:删 --enable-preview;然后为所有用到 FFM/JNI 的模块显式声明 --enable-native-access=<module>,否则升 25 后日志里会持续出现这类警告,而且未来的 JDK 会直接禁止。
Structured Concurrency:到 JDK 25 仍是预览,且 API 换了形状 结构化并发是这一批里唯一升上去反而要改代码的预览特性:从 JEP 453 (JDK 21 预览)一路预览到 JEP 505 (JDK 25 第五次预览),到 JDK 25 依然是预览特性 ,仍需要 --enable-preview。JEP 505 还把创建方式从公开构造器改成了静态工厂方法:JDK 21 里 new StructuredTaskScope.ShutdownOnFailure() 这种写法在 25 上直接编译失败。
JDK 21 时代的写法(在 21 上能用,25 上编译失败):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.util.concurrent.StructuredTaskScope;public class StructuredOld { public static void main (String[] args) throws Exception { try (var scope = new StructuredTaskScope .ShutdownOnFailure()) { var user = scope.fork(() -> "user-1" ); var order = scope.fork(() -> "order-9" ); scope.join(); scope.throwIfFailed(); System.out.println(user.get() + " / " + order.get()); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ jdk-21/bin/javac --release 21 --enable-preview -d b21p src/StructuredOld.java 注: src/StructuredOld.java 使用 Java SE 21 的预览功能。 注: 有关详细信息,请使用 -Xlint:preview 重新编译。 $ jdk-21/bin/java --enable-preview -cp b21p StructuredOld user-1 / order-9 $ jdk-25/bin/javac --release 25 --enable-preview -d b25p src/StructuredOld.java src/StructuredOld.java:6: 错误: 找不到符号 try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { ^ 符号: 类 ShutdownOnFailure 位置: 接口 StructuredTaskScope 注: src/StructuredOld.java 使用 Java SE 25 的预览功能。 注: 有关详细信息,请使用 -Xlint:preview 重新编译。 1 个错误
JDK 25 的写法(用静态工厂 open(),21 上编译失败):
1 2 3 4 5 6 7 8 9 10 11 12 13 import java.util.concurrent.StructuredTaskScope;public class StructuredNew { public static void main (String[] args) throws Exception { try (var scope = StructuredTaskScope.open()) { var user = scope.fork(() -> "user-1" ); var order = scope.fork(() -> "order-9" ); scope.join(); System.out.println(user.get() + " / " + order.get()); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ jdk-25/bin/javac --release 25 --enable-preview -d b25p src/StructuredNew.java 注: src/StructuredNew.java 使用 Java SE 25 的预览功能。 注: 有关详细信息,请使用 -Xlint:preview 重新编译。 $ jdk-25/bin/java --enable-preview -cp b25p StructuredNew user-1 / order-9 $ jdk-21/bin/javac --release 21 --enable-preview -d b21p src/StructuredNew.java src/StructuredNew.java:6: 错误: 找不到符号 try (var scope = StructuredTaskScope.open()) { ^ 符号: 方法 open() 位置: 类 StructuredTaskScope 注: src/StructuredNew.java 使用 Java SE 21 的预览功能。 注: 有关详细信息,请使用 -Xlint:preview 重新编译。 1 个错误
升级动作:不要用结构化并发来做需要跨 21/25 双版本编译的代码 ——它仍是预览,API 在 25 又改了一次(open() 工厂 + Joiner,fork 返回 Subtask 而非 Future)。已经写了的 21 风格代码要在 25 上改成 StructuredTaskScope.open(),并且继续保留 --enable-preview。
JDK 25 新增转正的两项,以及一项”21 就已正式、不用动”的 还有两个 JDK 25 定稿的语法糖,属于”升级后可选用”,顺手验证了一下:
灵活构造器体(JEP 513 ,JDK 25)允许在显式 super(...) 之前写语句,省掉了以前必须抽静态辅助方法的那步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class FlexCtor { static class Base { final String name; Base(String name) { this .name = name; } } static class Child extends Base { Child(String raw) { String normalized = raw == null ? "anon" : raw.trim(); super (normalized); } } public static void main (String[] args) { System.out.println(new Child (" bob " ).name); } }
1 2 3 4 5 6 7 8 9 $ jdk-21/bin/javac --release 21 -d b21 src/FlexCtor.java src/FlexCtor.java:11: 错误: 对super的调用必须是构造器中的第一个语句 super(normalized); ^ 1 个错误 $ jdk-25/bin/javac -d b25 src/FlexCtor.java $ jdk-25/bin/java -cp b25 FlexCtor bob
紧凑源文件(JEP 512 ,JDK 25)允许顶层 void main() 无显式类:
1 2 3 void main () { System.out.println("compact source file" ); }
1 2 3 4 5 6 7 8 9 $ jdk-21/bin/java src/HelloSimple.java src/HelloSimple.java:1: 错误: 未命名类 是预览功能,默认情况下禁用。 void main() { ^ (请使用 --enable-preview 以启用 未命名类) 1 个错误 $ jdk-25/bin/java src/HelloSimple.java compact source file
反向的确认:record 解构与 switch 模式匹配(JEP 440 / JEP 441 )在 JDK 21 已经正式 ,21 到 25 之间没有变化,同一份代码两版都直接编译运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class PatternDemo { record Point (int x, int y) { } static String classify (Object o) { return switch (o) { case Point (int x, int y) when x == y -> "diagonal " + x; case Point (int x, int y) -> "point " + x + "," + y; case String s -> "string " + s; default -> "other" ; }; } public static void main (String[] args) { System.out.println(classify(new Point (3 , 3 ))); System.out.println(classify(new Point (1 , 2 ))); System.out.println(classify("hi" )); } }
1 2 3 4 5 6 7 8 9 $ jdk-21/bin/java -cp b21 PatternDemo diagonal 3 point 1,2 string hi $ jdk-25/bin/java -cp b25 PatternDemo diagonal 3 point 1,2 string hi
行为变化:虚拟线程在 synchronized 内的固定 这是 21 到 25 之间运行时行为 变化最大的一处,而且是”代码一行不改、性能突然变好”的类型。
同一份程序:JDK 21 固定载体,JDK 25 解除 在 JDK 21 里,虚拟线程一旦进入 synchronized 代码块就无法从载体线程上卸载,遇到 Thread.sleep 这种阻塞会连载体一起卡住。JDK 24 的 JEP 491 (Status: Closed / Delivered、Release: 24)重写了 synchronized 的实现,让虚拟线程在 synchronized 方法/语句里、以及 Object.wait() 时都能正常卸载。下面用”每任务独立监视器 + synchronized 内 sleep”来放大这个差异:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import java.time.Duration;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.locks.ReentrantLock;public class Pinning { private static final int TASKS = 64 ; private static final Duration HOLD = Duration.ofMillis(500 ); public static void main (String[] args) throws Exception { System.out.println("cpus=" + Runtime.getRuntime().availableProcessors()); System.out.println("synchronized elapsed=" + runSync() + "ms" ); System.out.println("reentrant-lock elapsed=" + runLock() + "ms" ); } private static long runSync () throws Exception { long start = System.nanoTime(); try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0 ; i < TASKS; i++) { executor.submit(() -> { Object monitor = new Object (); synchronized (monitor) { hold(); } }); } } return (System.nanoTime() - start) / 1_000_000 ; } private static long runLock () throws Exception { long start = System.nanoTime(); try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0 ; i < TASKS; i++) { executor.submit(() -> { ReentrantLock lock = new ReentrantLock (); lock.lock(); try { hold(); } finally { lock.unlock(); } }); } } return (System.nanoTime() - start) / 1_000_000 ; } private static void hold () { try { Thread.sleep(HOLD); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
本机是 10 核,虚拟线程调度器默认并行度等于核心数(10)。64 个任务各睡 500ms:JDK 21 上 synchronized 分支会固定载体,64 个任务只能被 10 个载体分批消化,大约 7 批 × 500ms ≈ 3.5 秒;ReentrantLock 分支不固定,理论上 500ms 左右。跑了三轮,结果稳定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 --- JDK 21 --- cpus=10 synchronized elapsed=3550ms reentrant-lock elapsed=511ms cpus=10 synchronized elapsed=3550ms reentrant-lock elapsed=501ms cpus=10 synchronized elapsed=3547ms reentrant-lock elapsed=509ms --- JDK 25 --- cpus=10 synchronized elapsed=516ms reentrant-lock elapsed=511ms cpus=10 synchronized elapsed=520ms reentrant-lock elapsed=511ms cpus=10 synchronized elapsed=519ms reentrant-lock elapsed=501ms
reentrant-lock 两版几乎一样(约 505~511ms),排除了环境差异;差异集中在 synchronized 分支:21 约 3550ms,25 约 520ms,接近一个数量级。结论:如果你在 JDK 21 上因为固定问题把热点 synchronized 换成了 ReentrantLock,JDK 25 上不必再这么做了 ;JEP 491 的正文也明确说,已迁移到 ReentrantLock 的代码无需迁回。
-Djdk.tracePinnedThreads 在 JDK 24 起不再生效JDK 21 用 -Djdk.tracePinnedThreads=full 可以在固定发生时打印栈。JEP 491 正文有一节专门说这个系统属性”no longer needed”,并明确:从 JDK 24 起移除该属性,命令行上设置它不会有任何效果 。实测对比:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class PinTrace { public static void main (String[] args) throws Exception { Thread[] threads = new Thread [4 ]; for (int i = 0 ; i < threads.length; i++) { threads[i] = Thread.ofVirtual().start(() -> { Object monitor = new Object (); synchronized (monitor) { try { Thread.sleep(200 ); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }); } for (Thread t : threads) { t.join(); } System.out.println("pin trace run done" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 $ jdk-21/bin/java -Djdk.tracePinnedThreads=full -cp b21 PinTrace VirtualThread[#22]/runnable@ForkJoinPool-1-worker-2 reason:MONITOR java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:199) java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393) java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:635) java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:807) java.base/java.lang.Thread.sleep(Thread.java:507) PinTrace.lambda$main$0(PinTrace.java:10) <== monitors:1 java.base/java.lang.VirtualThread.run(VirtualThread.java:329) pin trace run done $ jdk-25/bin/java -Djdk.tracePinnedThreads=full -cp b25 PinTrace pin trace run done
21 上打印了 9 行(固定栈),25 上只打印程序自己的那一行 pin trace run done,属性完全失效。
用什么替代:JFR 的 jdk.VirtualThreadPinned JEP 491 保留并增强了 JFR 事件 jdk.VirtualThreadPinned(剩下的固定场景,如 native 回调、类加载等,仍会被记录)。用同一个 PinTrace 程序各录 6 秒,对比事件数:
1 2 3 4 5 6 7 8 9 $ jdk-21/bin/java -XX:StartFlightRecording=filename=pin21.jfr,settings=default,duration=6s -cp b21 PinTrace [0.258s][info][jfr,startup] Started recording 1. The result will be written to: /private/tmp/jdk-migration/out/pin21.jfr pin trace run done $ jdk-25/bin/java -XX:StartFlightRecording=filename=pin25.jfr,settings=default,duration=6s -cp b25 PinTrace [0.229s][info][jfr,startup] Started recording 1. The result will be written to: /private/tmp/jdk-migration/out/pin25.jfr pin trace run done
1 2 3 4 5 $ jdk-21/bin/jfr summary pin21.jfr | grep VirtualThread jdk.VirtualThreadPinned 4 56 $ jdk-25/bin/jfr summary pin25.jfr | grep VirtualThread jdk.VirtualThreadPinned 0 0
同一份程序:JDK 21 录到 4 次固定事件,JDK 25 录到 0 次。升级后排查固定,请改用 jfr 事件而不是那个已失效的系统属性。
移除与降级:逐项核对官方源 这一节只写能在 openjdk.org 的 JEP 页面或官方 API 文档里核实的条目,并明确区分”编译期报错 / 运行时警告 / 仅行为变化”。
Security Manager:永久禁用(JEP 486,JDK 24,直接报错) JEP 486 (Release: 24)接续 JDK 17 的 JEP 411 (弃用),把 Security Manager 永久禁用:System.setSecurityManager 永远抛 UnsupportedOperationException,System.getSecurityManager 永远返回 null,-Djava.security.manager 只能设为 disallow。
1 2 3 4 5 6 7 8 9 10 11 public class SecMgr { public static void main (String[] args) { try { System.setSecurityManager(new SecurityManager ()); System.out.println("security manager installed" ); } catch (Throwable t) { System.out.println("install failed: " + t.getClass().getName()); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ jdk-21/bin/java -cp b21 SecMgr install failed: java.lang.UnsupportedOperationException $ jdk-21/bin/java -Djava.security.manager=allow -cp b21 SecMgr WARNING: A terminally deprecated method in java.lang.System has been called WARNING: System::setSecurityManager has been called by SecMgr (file:/private/tmp/jdk-migration/b21/) WARNING: Please consider reporting this to the maintainers of SecMgr WARNING: System::setSecurityManager will be removed in a future release security manager installed $ jdk-25/bin/java -cp b25 SecMgr install failed: java.lang.UnsupportedOperationException $ jdk-25/bin/java -Djava.security.manager=allow -cp b25 SecMgr Error occurred during initialization of VM java.lang.Error: A command line option has attempted to allow or enable the Security Manager. Enabling a Security Manager is not supported. at java.lang.System.initPhase3(java.base@25/System.java:1969)
21 上还能靠 -Djava.security.manager=allow 强行装回(伴随”即将删除”的警告);25 上连 JVM 都起不来,直接在初始化阶段报 Error。这属于启动即报错 级别。升级动作:删除所有 -Djava.security.manager=allow 与 setSecurityManager 调用;依赖 Security Manager 做沙箱的历史组件必须换成进程级/容器级隔离。
sun.misc.Unsafe 内存访问方法:JDK 23 弃用、JDK 24 起运行时警告 sun.misc.Unsafe 的内存访问方法由 JEP 471 (JDK 23)标记为”待删除”,JEP 498 (JDK 24)起在首次调用时打印运行时警告 。用反射拿到 theUnsafe 后读写字段与堆外内存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import java.lang.reflect.Field;import sun.misc.Unsafe;public class UnsafeDemo { private static final Unsafe UNSAFE; static { try { Field field = Unsafe.class.getDeclaredField("theUnsafe" ); field.setAccessible(true ); UNSAFE = (Unsafe) field.get(null ); } catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError (e); } } public static void main (String[] args) { Holder holder = new Holder (); long offset; try { offset = UNSAFE.objectFieldOffset(Holder.class.getDeclaredField("value" )); } catch (NoSuchFieldException e) { throw new IllegalStateException (e); } UNSAFE.putInt(holder, offset, 42 ); System.out.println("value=" + holder.value); long address = UNSAFE.allocateMemory(4 ); try { UNSAFE.putInt(address, 7 ); System.out.println("offHeap=" + UNSAFE.getInt(address)); } finally { UNSAFE.freeMemory(address); } } static class Holder { int value; } }
1 2 3 4 5 6 7 8 9 10 11 $ jdk-21/bin/java -cp b21 UnsafeDemo value=42 offHeap=7 $ jdk-25/bin/java -cp b25 UnsafeDemo WARNING: A terminally deprecated method in sun.misc.Unsafe has been called WARNING: sun.misc.Unsafe::objectFieldOffset has been called by UnsafeDemo (file:/private/tmp/jdk-migration/b25/) WARNING: Please consider reporting this to the maintainers of class UnsafeDemo WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release value=42 offHeap=7
21 上静默;25 上首次调用打印 4 行警告。这属于运行时警告 ,不是报错——程序照常跑。JDK 24 同时引入开关 --sun-misc-unsafe-memory-access=allow|warn|debug|deny:
1 2 3 4 5 6 7 8 9 10 $ jdk-25/bin/java --sun-misc-unsafe-memory-access=deny -cp b25 UnsafeDemo Exception in thread "main" java.lang.UnsupportedOperationException: objectFieldOffset at jdk.unsupported/sun.misc.Unsafe.beforeMemoryAccessSlow(Unsafe.java:1822) at jdk.unsupported/sun.misc.Unsafe.beforeMemoryAccess(Unsafe.java:1787) at jdk.unsupported/sun.misc.Unsafe.objectFieldOffset(Unsafe.java:905) at UnsafeDemo.main(UnsafeDemo.java:22) $ jdk-21/bin/java --sun-misc-unsafe-memory-access=deny -cp b21 UnsafeDemo Unrecognized option: --sun-misc-unsafe-memory-access=deny Error: Could not create the Java Virtual Machine.
升级动作:优先迁到标准替代(字段/数组访问用 VarHandle,堆外内存用 FFM API);迁移完成前,把用到 Unsafe 的第三方库升级到最新版本(多数网络/序列化库已跟进),并留意启动日志里的警告。JNI 那半边(JEP 472 对 JNI 的首次使用警告)与本项同源,但本机没有用 JDK 自带工具构建 .dylib 的条件,未实测 ,仅按 JEP 472 正文记录其存在。
Thread 遗留方法:一部分早于 21 就废了,一部分在 22/23 被删 Thread.stop() 从 JDK 20 起就无条件抛 UnsupportedOperationException——比 JDK 21 还早,所以它不属于 21→25 的变化 ,两版行为完全一致。这可以从官方 API 文档的措辞变化看出来:JDK 19 的 stop() 只在”虚拟线程上”抛 UOE,JDK 20 起改为 “always”:
1 2 3 4 5 # JDK 19 的 Thread.stop() javadoc(节选) UnsupportedOperationException - if invoked on a virtual thread # JDK 20 / 21 / 25 的 Thread.stop() javadoc(节选) Throws: UnsupportedOperationException - always
而 ThreadGroup.stop()、Thread.countStackFrames() 这类方法是在 21 之后被真正删除 的(这些删减走的是 JDK bug 流程,没有独立 JEP 编号)。用官方 API 文档的”方法是否存在”矩阵来定位首次移除版本:
1 2 3 4 5 6 7 8 JDK 21 JDK 22 JDK 23 JDK 24 JDK 25 Thread.countStackFrames() 有 无 无 无 无 <- JDK 22 移除 ThreadGroup.stop() 有 有 无 无 无 <- JDK 23 移除 Thread.suspend() 有 有 无 无 无 <- JDK 23 移除 Thread.resume() 有 有 无 无 无 <- JDK 23 移除 ThreadGroup.suspend() 有 有 无 无 无 <- JDK 23 移除 ThreadGroup.resume() 有 有 无 无 无 <- JDK 23 移除 Thread.stop() 有 有 有 有 有 <- 未删除,仅"待删除"
(这张表不是背出来的:对 docs.oracle.com 上各版本的 Thread / ThreadGroup javadoc 页做方法存在性检查(例如在页面 HTML 里统计 id="suspend()" 是否出现),就能定位某个方法是在哪个版本消失的。)
实测编译差异。下面这个类只调用了那两个已被删除的方法,JDK 21 上只有”待删除”警告、JDK 25 上直接编译失败:
1 2 3 4 5 6 7 public class RemovedApi { public static void main (String[] args) { Thread.currentThread().getThreadGroup().stop(); System.out.println(Thread.currentThread().countStackFrames()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $ jdk-21/bin/javac --release 21 -d b21 src/RemovedApi.java src/RemovedApi.java:4: 警告: [removal] ThreadGroup 中的 stop() 已过时, 且标记为待删除 Thread.currentThread().getThreadGroup().stop(); ^ src/RemovedApi.java:5: 警告: [removal] Thread 中的 countStackFrames() 已过时, 且标记为待删除 System.out.println(Thread.currentThread().countStackFrames()); ^ 2 个警告 $ jdk-25/bin/javac --release 25 -d b25 src/RemovedApi.java src/RemovedApi.java:4: 错误: 找不到符号 Thread.currentThread().getThreadGroup().stop(); ^ 符号: 方法 stop() 位置: 类 ThreadGroup src/RemovedApi.java:5: 错误: 找不到符号 System.out.println(Thread.currentThread().countStackFrames()); ^ 符号: 方法 countStackFrames() 位置: 类 Thread 2 个错误
如果旧代码是已经编译好的 jar(比如第三方库),升级后是运行时 炸,而不是编译炸。下面这段在 JDK 21 上能完整编译的遗留代码(包含四个已被降级或删除的方法):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class Legacy { public static void main (String[] args) throws Exception { Runtime.getRuntime().runFinalization(); Thread worker = new Thread (() -> { }); worker.start(); worker.join(); try { worker.stop(); System.out.println("stop() returned normally" ); } catch (Throwable t) { System.out.println("stop() threw " + t.getClass().getName()); } ThreadGroup group = Thread.currentThread().getThreadGroup(); try { group.stop(); System.out.println("ThreadGroup.stop() returned normally" ); } catch (Throwable t) { System.out.println("ThreadGroup.stop() threw " + t.getClass().getName()); } try { System.out.println("countStackFrames()=" + Thread.currentThread().countStackFrames()); } catch (Throwable t) { System.out.println("countStackFrames() threw " + t.getClass().getName()); } System.out.println("legacy run on " + Runtime.version()); } }
用 JDK 21 编译它(只有 4 条”待删除”警告)、打成 jar,然后分别在两个 JVM 上跑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 $ jdk-21/bin/javac --release 21 -d b21 src/Legacy.java src/Legacy.java:4: 警告: [removal] Runtime 中的 runFinalization() 已过时, 且标记为待删除 Runtime.getRuntime().runFinalization(); ^ src/Legacy.java:11: 警告: [removal] Thread 中的 stop() 已过时, 且标记为待删除 worker.stop(); ^ src/Legacy.java:19: 警告: [removal] ThreadGroup 中的 stop() 已过时, 且标记为待删除 group.stop(); ^ src/Legacy.java:26: 警告: [removal] Thread 中的 countStackFrames() 已过时, 且标记为待删除 System.out.println("countStackFrames()=" + Thread.currentThread().countStackFrames()); ^ 4 个警告 $ jdk-21/bin/java -cp jar/legacy-demo.jar Legacy stop() threw java.lang.UnsupportedOperationException ThreadGroup.stop() threw java.lang.UnsupportedOperationException countStackFrames() threw java.lang.UnsupportedOperationException legacy run on 21.0.8+12-LTS-250 $ jdk-25/bin/java -cp jar/legacy-demo.jar Legacy stop() threw java.lang.UnsupportedOperationException ThreadGroup.stop() threw java.lang.NoSuchMethodError countStackFrames() threw java.lang.NoSuchMethodError legacy run on 25+37-LTS-3491
同一个 jar:21 上是”已删除”级别的 UnsupportedOperationException(还能 catch 住继续跑),25 上变成 NoSuchMethodError——这是 Error 不是 Exception,catch (Throwable) 之外的代码会被直接打穿 。升级动作:这类方法只能逐个替换(stop → 协作式取消标志/interrupt,countStackFrames → StackWalker)。
32 位 x86 端口移除(JEP 501/503、JEP 479,未实测) JEP 501 (JDK 24)弃用 32 位 x86 端口,JEP 503 (JDK 25)将其移除;Windows 的 32 位 x86 端口则由 JEP 479 (JDK 24)移除。三条 JEP 的 Release 字段分别为 24 / 25 / 24。本机是 arm64,没有 32 位 x86 的 JDK 可跑,这一项未实测 ,仅按 JEP 页面记录:如果你还在维护 32 位 x86 的部署目标,JDK 25 没有对应构建,需要在 24 或更早就开始规划迁到 64 位。
工具链自检:jdeps / jdeprscan / jcmd 升级前用三个自带工具做体检,比读 release notes 更贴近你自己的代码。
jdeps:查 JDK 内部 API 依赖 对含 sun.misc.Unsafe 引用的 jar 跑 jdeps --jdk-internals,会点出”JDK internal API”并给替换建议:
1 2 3 4 5 6 7 8 9 10 11 $ jdk-25/bin/jdeps --jdk-internals jar/unsafe-demo.jar unsafe-demo.jar -> jdk.unsupported UnsafeDemo -> sun.misc.Unsafe JDK internal API (jdk.unsupported) 警告: 不支持 JDK 内部 API, 它们专用于通过不兼容方式来 删除或更改的 JDK 实现, 可能会损坏您的应用程序。 请修改您的代码, 消除与任何 JDK 内部 API 的相关性。 JDK 内部 API 建议的替换 ---------- ----- sun.misc.Unsafe See https://openjdk.org/jeps/260
jdeps -summary 则给模块级依赖概览:
1 2 3 $ jdk-25/bin/jdeps -summary jar/unsafe-demo.jar unsafe-demo.jar -> java.base unsafe-demo.jar -> jdk.unsupported
两个用法互斥(-summary/-verbose 不能和 -jdkinternals 同时用),分开跑即可。如果 jar 里没有任何内部 API,--jdk-internals 就不输出”内部 API”表,只显示模块依赖——注意看上面 unsafe 的输出来自专门的 demo jar,纯粹的 Legacy jar 是干净的。
jdeprscan:查”已弃用且待删除”的 API,但别被它骗了 jdeprscan --for-removal 只扫 forRemoval=true 的 API。对同一个 jar,用哪个 JDK 的工具、哪个 --release,结果不一样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ jdk-21/bin/jdeprscan --release 21 --for-removal jar/legacy-demo.jar Jar 文件 jar/legacy-demo.jar: class Legacy 使用已过时的方法 java/lang/Runtime::runFinalization()V (forRemoval=true) class Legacy 使用已过时的方法 java/lang/Thread::stop()V (forRemoval=true) class Legacy 使用已过时的方法 java/lang/ThreadGroup::stop()V (forRemoval=true) class Legacy 使用已过时的方法 java/lang/Thread::countStackFrames()I (forRemoval=true) class SecMgr 使用已过时的类 java/lang/SecurityManager (forRemoval=true) class SecMgr 使用已过时的方法 java/lang/System::setSecurityManager(Ljava/lang/SecurityManager;)V (forRemoval=true) $ jdk-25/bin/jdeprscan --release 21 --for-removal jar/legacy-demo.jar Jar 文件 jar/legacy-demo.jar: class Legacy 使用已过时的方法 java/lang/Runtime::runFinalization()V (forRemoval=true) class Legacy 使用已过时的方法 java/lang/Thread::stop()V (forRemoval=true) class SecMgr 使用已过时的类 java/lang/SecurityManager (forRemoval=true) class SecMgr 使用已过时的方法 java/lang/System::setSecurityManager(Ljava/lang/SecurityManager;)V (forRemoval=true)
同一个 jar,JDK 21 的 jdeprscan 扫出 6 条,JDK 25 的 jdeprscan(哪怕加 --release 21)只扫出 4 条。少掉的 ThreadGroup::stop 和 countStackFrames 恰恰是 25 上会 NoSuchMethodError 的那两个 ——因为这两个方法在工具所在 JDK 里已经不存在了,jdeprscan 无法解析,于是”静默跳过”。结论:jdeprscan 的”扫描通过/变干净”不能证明代码能跑在新 JDK 上,它只负责查”仍存在但已弃用”的 API;查”已删除”的唯一可靠手段是用目标 JDK 编译一次 。顺带,jdeprscan --list 的条目总数也能佐证这个”数据库随 JDK 走”的事实:
1 2 3 4 5 6 7 8 9 $ jdk-21/bin/jdeprscan --list | wc -l 681 $ jdk-21/bin/jdeprscan --list --for-removal | wc -l 103 $ jdk-25/bin/jdeprscan --list | wc -l 720 $ jdk-25/bin/jdeprscan --list --for-removal | wc -l 132
jcmd:运行时观测(版本、堆、默认参数、线程 dump) 让同一个 LongRunning 程序(打印 pid 后休眠)在两版上跑起来,用 jcmd 观测:
1 2 3 4 5 6 7 8 9 $ jdk-21/bin/jcmd 75817 VM.version 75817: Java HotSpot(TM) 64-Bit Server VM version 21.0.8+12-LTS-250 JDK 21.0.8 $ jdk-25/bin/jcmd 75863 VM.version 75863: Java HotSpot(TM) 64-Bit Server VM version 25+37-LTS-3491 JDK 25.0.0
GC.heap_info 能直接确认默认 GC,并且两版输出格式并不完全一致(25 少了 Metaspace 两行、total 拆成 reserved/committed):
1 2 3 4 5 6 7 8 9 10 11 $ jdk-21/bin/jcmd 75817 GC.heap_info 75817: garbage-first heap total 264192K, used 4467K [0x0000000700000000, 0x0000000800000000) region size 2048K, 2 young (4096K), 0 survivors (0K) Metaspace used 520K, committed 640K, reserved 1114112K class space used 54K, committed 128K, reserved 1048576K $ jdk-25/bin/jcmd 75863 GC.heap_info 75863: garbage-first heap total reserved 4194304K, committed 264192K, used 5249K [0x0000000700000000, 0x0000000800000000) region size 2048K, 2 young (4096K), 0 survivors (0K)
两版都支持 Thread.dump_to_file -format=json(输出的 JSON 按 threadContainers 分组,含 <root>、ForkJoinPool-1/jdk.internal.vm.SharedThreadContainer 等容器):
1 2 3 $ jdk-21/bin/jcmd 75817 Thread.dump_to_file -format=json threads21.json 75817: Created /private/tmp/jdk-migration/out/threads21.json
如果升级后要看某个运行中进程有没有设置已移除的参数,用 VM.flags -all 过滤(下一节会用到)。
GC 与启动参数 默认 GC 没变:G1 仍是服务器环境默认 两版的 -XX:+PrintFlagsFinal 里 UseG1GC 都是 true {product} {ergonomic},GC.heap_info 也都显示 garbage-first heap:
1 2 3 4 5 6 7 8 9 10 11 $ jdk-21/bin/java -XX:+PrintFlagsFinal -version | grep -E 'UseG1GC|UseZGC|UseParallelGC|ZGenerational' bool UseG1GC = true {product} {ergonomic} bool UseParallelGC = false {product} {default} bool UseZGC = false {product} {default} bool ZGenerational = false {product} {default} $ jdk-25/bin/java -XX:+PrintFlagsFinal -version | grep -E 'UseG1GC|UseZGC|UseParallelGC|UseCompactObjectHeaders' bool UseCompactObjectHeaders = false {product lp64_product} {default} bool UseG1GC = true {product} {ergonomic} bool UseParallelGC = false {product} {default} bool UseZGC = false {product} {default}
G1 成为默认 GC 是 JDK 9 的 JEP 248 (Release: 9),至今未变。这里有个容易误读的点:JEP 523 (Release: 27)要把 G1 变成”所有环境”的默认,它在 JDK 27 才生效 ——也就是说在 JDK 25 里,非服务器(client)环境仍可能用别的收集器。绝大多数服务端部署不受影响,这里不展开。
被移除/新引入的参数(每条都实测过) ZGC 非分代模式移除(JEP 474 / 490) 。ZGC 的分代模式在 JDK 23 成为默认(JEP 474 ),非分代模式在 JDK 24 被移除(JEP 490 )。-XX:-ZGenerational 这个参数在 21 上被接受、在 25 上被忽略并警告:
1 2 3 4 5 6 $ jdk-21/bin/java -XX:-ZGenerational -version java version "21.0.8" 2025-07-15 LTS $ jdk-25/bin/java -XX:-ZGenerational -version Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option ZGenerational; support was removed in 24.0 java version "25" 2025-09-16 LTS
升级动作:如果你用了 ZGC,删掉 -XX:-ZGenerational(或显式 -XX:+ZGenerational,它现在就是默认);25 上是警告 + 忽略 ,不会崩,但参数已无意义。
压缩对象头(JEP 450 / 519) 。压缩对象头在 JDK 24 是实验特性(JEP 450 ),JDK 25 转为产品特性(JEP 519 )。开关 -XX:+UseCompactObjectHeaders 在 21 上不存在、在 25 上直接可用(不需要 -XX:+UnlockExperimentalVMOptions):
1 2 3 4 5 6 7 $ jdk-21/bin/java -XX:+UseCompactObjectHeaders -version Unrecognized VM option 'UseCompactObjectHeaders' Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit. $ jdk-25/bin/java -XX:+UseCompactObjectHeaders -version java version "25" 2025-09-16 LTS
这是可选的省内存能力,默认关闭(上面 PrintFlagsFinal 里是 = false),升级后可评估启用。
AOT 缓存参数(JEP 483 / 514 / 515) 。JDK 24 的 JEP 483 引入 AOT 类加载与链接(-XX:AOTMode、-XX:AOTConfiguration),JDK 25 的 JEP 514 补齐命令行体验(-XX:AOTCache、-XX:AOTCacheOutput)。这些参数在 21 上完全不存在:
1 2 3 4 5 6 7 8 9 $ jdk-25/bin/java -XX:+PrintFlagsFinal -version | grep -iE 'AOTMode|AOTCache|AOTConfiguration|AOTClassLinking' ccstr AOTCache = {product} {default} ccstr AOTCacheOutput = {product} {default} bool AOTClassLinking = false {product} {default} ccstr AOTConfiguration = {product} {default} ccstr AOTMode = {product} {default} $ jdk-21/bin/java -XX:+PrintFlagsFinal -version | grep -iE 'AOTMode|AOTCache|AOTConfiguration|AOTClassLinking' (无输出)
这是加速启动的可选项,对迁移本身没有破坏性;如果 21 的脚本里带了这些参数,那 21 上早就报错了,反过来不成立。
--release 支持范围:21 与 25 一致(都是 8 起) 。用下面这个三行类做探针:
1 2 3 4 5 6 public class Hello8 { public static void main (String[] args) { System.out.println("hello" ); } }
--release 7 两版都直接拒绝,--release 8 两版都只警告”已过时”而仍可用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $ jdk-21/bin/javac --release 7 -d b21 src/Hello8.java 错误: 不支持发行版本 7 用法: javac <选项> <源文件> 使用 --help 可列出可能的选项 $ jdk-25/bin/javac --release 7 -d b25 src/Hello8.java 错误: 不支持发行版本 7 用法: javac <选项> <源文件> 使用 --help 可列出可能的选项 $ jdk-21/bin/javac --release 8 -d b21 src/Hello8.java 警告: [options] 源值 8 已过时,将在未来发行版中删除 警告: [options] 目标值 8 已过时,将在未来发行版中删除 警告: [options] 要隐藏有关已过时选项的警告, 请使用 -Xlint:-options。 3 个警告 $ jdk-25/bin/javac --release 8 -d b25 src/Hello8.java 警告: [options] 源值 8 已过时,将在未来发行版中删除 警告: [options] 目标值 8 已过时,将在未来发行版中删除 警告: [options] 要隐藏有关已过时选项的警告, 请使用 -Xlint:-options。 3 个警告
也就是说”源码版本 8 被移除”这件事在 25 还没发生 ,不必急着改 --release 8。已移除的部分是另外一回事(比如上面说的 JDK 22/23 删掉的 API,它们在低 --release 下同样编不过)。
升级前自查清单(可复制) 把上面的验证压缩成一条可复制的流程,全部只用 JDK 自带工具:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /path/to/jdk-25/bin/javac --release 25 -d /tmp/build25 $(find src -name '*.java' ) /path/to/jdk-25/bin/jdeprscan --release 25 --for-removal app.jar /path/to/jdk-25/bin/jdeps --jdk-internals app.jar /path/to/jdk-25/bin/java -XX:+PrintFlagsFinal -version \ | grep -E 'UseG1GC|ZGenerational|UseCompactObjectHeaders|AOTMode|AOTCache' jcmd <pid> GC.heap_info jcmd <pid> VM.flags -all jcmd <pid> Thread.dump_to_file -format=json threads.json
先编译(步骤 1),再看 jdeprscan(步骤 2) :已删除的 API 不会出现在扫描结果里,只跑 jdeprscan 会得到”干净”的假象。
总表
项目
JDK 21 现状
JDK 25 现状
升级动作
验证方式
未命名变量 _(JEP 443→456)
预览,需 --enable-preview
正式
删 --enable-preview
编译 + 运行对照
Stream Gatherers(JEP 461/473→485)
不存在,加预览也编不过
正式(Stream.gather)
无(21 上写不出来),升后可用
编译对照
Scoped Values(JEP 446→506)
预览
正式
删 --enable-preview;改掉 orElse(null)
编译 + 运行对照
FFM API(JEP 442→454)
预览
正式
删 --enable-preview;声明 --enable-native-access
编译 + 运行对照
Structured Concurrency(JEP 453→505)
预览(new ShutdownOnFailure())
仍预览,API 改为 open()
改写法,继续保留 --enable-preview
双向编译对照
灵活构造器体(JEP 513)
报”super 必须是第一个语句”
正式
可用(可选)
编译对照
紧凑源文件(JEP 512)
预览
正式
可用(可选)
java x.java 对照
record/switch 模式匹配(JEP 440/441)
已正式
已正式
无
运行对照(一致)
虚拟线程 synchronized 固定(JEP 491)
固定载体
解除
无需再换 ReentrantLock
同程序耗时:3550ms vs 520ms
-Djdk.tracePinnedThreads
有效
移除、无效果
改用 JFR jdk.VirtualThreadPinned
9 行 vs 1 行;JFR 4 次 vs 0 次
Security Manager(JEP 486)
allow 可装回
永久禁用
删属性与 setSecurityManager
启动对照(25 直接 Error)
sun.misc.Unsafe 内存访问(JEP 471/498)
静默
首次调用警告;可 deny
迁 VarHandle/FFM
运行输出对照
Thread.stop()
抛 UOE(JDK 20 起)
抛 UOE
无(21 前就废了)
两版运行一致
ThreadGroup.stop() / countStackFrames()
弃用警告,能编
已删除
换协作取消 / StackWalker
编译”找不到符号”;旧 jar 运行 NoSuchMethodError
32 位 x86 端口(JEP 501/503、479)
有
无构建
迁 64 位
未实测(arm64),文档核实
默认 GC
G1(服务器环境)
G1(服务器环境)
无
PrintFlagsFinal + GC.heap_info
ZGC 非分代模式(JEP 474/490)
-XX:-ZGenerational 可用
移除,忽略并警告
删参数
-version 对照
压缩对象头(JEP 450/519)
参数不存在
产品特性(默认关)
可选启用
-version 对照
AOT 参数(JEP 483/514/515)
不存在
AOTMode/AOTCache 等
可选启用
PrintFlagsFinal 对照
javac --release 8
可用(警告)
可用(警告)
无
编译对照
总结
21 到 25 的升级主体是删开关 :_、Scoped Values、FFM 都从”预览”转正,对应的 --enable-preview 可以全删;只有 Structured Concurrency 到 25 仍是预览,而且 API 从 new ShutdownOnFailure() 换成了 open(),是这一批里唯一”升了反而要改代码”的预览特性。
行为变化最大的一处是虚拟线程的固定:JEP 491 在 24 解除了 synchronized / Object.wait() 的固定,同一个”独占监视器 + synchronized 内 sleep”程序,21 约 3550ms、25 约 520ms;-Djdk.tracePinnedThreads 随之失效,改看 JFR 的 jdk.VirtualThreadPinned(同程序 4 次 → 0 次)。
移除类里要分清三档:编译期报错 (ThreadGroup.stop、countStackFrames 在 22/23 被删,javac 报”找不到符号”)、运行时警告 (sun.misc.Unsafe 内存访问在 24 起首次调用打印 4 行警告,可 deny)、启动即报错 (Security Manager 在 24 永久禁用,-Djava.security.manager=allow 直接起不来)。
Thread.stop() 属于”早于 21 就废了”的那类:JDK 20 起永远抛 UOE,21 和 25 行为一致,不是这次迁移的新坑;真正的新坑是旧 jar 里 ThreadGroup.stop/countStackFrames 从 21 的 UOE 变成 25 的 NoSuchMethodError(Error,会打穿 catch (Exception))。
GC 与参数:默认 GC 还是 G1({ergonomic},garbage-first heap);-XX:-ZGenerational 被移除(忽略 + 警告),新增 UseCompactObjectHeaders 和一批 AOT 参数;--release 8 仍在支持范围内。
工具链结论:jdeps --jdk-internals 查内部 API,jdeprscan --for-removal 查”仍存在但已弃用”的 API——后者会漏报已删除的方法(ThreadGroup::stop、countStackFrames 在 25 的扫描结果里凭空消失),所以先编译、后扫描 ,别把 jdeprscan 当安全网。
参考资料
系列索引:Java 系列 ,语言特性与运行时的长文集