从 8 / 11 / 17 迁移到 21
学完你要能自己回答
- 从 JDK 8 升到 21,哪些 Java EE 模块与 GC 参数必须先清理?
--release与-source/-target的区别是什么?为什么升级时优先用--release 21?- 强封装之后出现
InaccessibleObjectException,应该怎么定位和处理? - 预览特性为什么不能跨版本运行?编译和运行各需要什么参数?
先确认版本坐标01
迁移的第一步不是改代码,而是把「源码、编译工具、运行时、依赖」四者的版本对齐。class 文件版本不匹配,是最常见的启动失败原因。
| 版本 | class 文件 | 默认 GC | 迁移时最值得注意的变化 |
|---|---|---|---|
| JDK 8 | 52 | Parallel | PermGen 已移除;Java EE 模块仍在 |
| JDK 11 | 55 | G1 | Java EE / CORBA 移除;Nashorn 弃用 |
| JDK 17 | 61 | G1 | 强封装 JDK 内部;RMI Activation 移除;Security Manager 弃用 |
| JDK 21 | 65 | G1 | 动态加载 agent 警告;32 位 Windows 弃用;预览特性密集 |
| JDK 25 | 69 | G1 | Scoped Values 转正;32 位 x86 端口移除 |
java -version
javac -version
# 查看某个 class 的 class 文件版本
javap -verbose build/classes/App.class | findstr major
# Linux / macOS: javap -verbose build/classes/App.class | grep major
UnsupportedClassVersionError 的报错会直接给出 class 文件版本,例如 class file version 65.0。它说明运行应用的 JDK 比编译时使用的 JDK 旧,或者依赖里有更高版本编译的 jar。
用 --release,而不是 -source / -target02
--release 21 不仅设置 class 文件版本,还会使用 JDK 21 自带的历史 API 快照(ct.sym)检查你只用了 Java 21 标准库里的 API。-source 21 -target 21 只约束语法和字节码版本,仍然可能编译到更新版本的 API,运行时才炸。
# JDK 21 自带的 API 快照:Math.clamp 可用
javac --release 21 -d out App.java
# 以 Java 17 API 为边界:Math.clamp 不存在
javac --release 17 -d out App.java
# error: cannot find symbol: method clamp(int,int,int)
| 工具链 / 命令 | 结果 | 说明 |
|---|---|---|
JDK 21 · --release 21 | ✓ | Math.clamp 编译通过 |
JDK 21 · --release 17 | × | 不能用 21 才有的 API |
JDK 17 · javac | × | cannot find symbol: clamp |
JDK 11 · javac | × | cannot find symbol: clamp |
JDK 8 · javac | × | cannot find symbol: clamp |
- JDK 21 的
javac最低支持--release 8,不能再用--release 7或更早。 - Maven 用
maven.compiler.release=21,Gradle 用options.release = 21,不要只配 source/target。 - 多模块项目里,消费方用 21 编译、依赖方仍用 17 编译是正常的;关键是运行时统一到 21 或更高。
-Xlint:removal会把调用了「标记为删除」的 API 的报告提升为编译警告。
Java EE 模块、Nashorn 与依赖升级03
JDK 11 通过 JEP 320 移除了 Java EE 与 CORBA 模块。很多老项目在 JDK 8 上能跑,是因为这些 API 还在 JDK 里;换到 11+ 后,必须显式引入第三方实现。
| 被移除的 API / 模块 | 移除版本 | 替代方案 |
|---|---|---|
javax.xml.bind(JAXB) | 11 | jakarta.xml.bind-api + 实现(如 GlassFish JAXB) |
javax.xml.ws(JAX-WS) | 11 | jakarta.xml.ws-api + CXF / Metro |
javax.activation | 11 | jakarta.activation-api + 实现 |
javax.annotation(Common Annotations) | 11 | jakarta.annotation-api |
CORBA / org.omg.* | 11 | 没有等价内置替代;迁移到其他 RPC / 消息方案 |
| Nashorn JavaScript | 15 | GraalJS 等外部脚本引擎 |
# 找出依赖里还在引用 JDK 内部 API 的类
jdeps --jdk-internals -R build/libs/app.jar
# 找出使用了「已弃用 / 将被删除」API 的类
jdeprscan --release 21 --for-removal build/libs/app.jar
# Maven 查看依赖树,重点排查旧的 javax.* 坐标
mvn dependency:tree | findstr javax
javax → jakarta 不是简单的包名替换:API 签名、注解名、实现版本都可能变化。先升级依赖坐标,再改 import,最后跑集成测试。
ScriptEngineManager().getEngineByName("nashorn"),在 JDK 21 上会得到 null,而不是启动失败。强封装与反射:先升级库,再加 --add-opens04
JDK 16 起,默认拒绝通过反射访问 JDK 内部包;JDK 17 的 JEP 403 进一步强封装,并移除了 --illegal-access 开关。依赖 sun.*、jdk.internal.* 或对 java.lang 等包做 setAccessible 的库,会抛 InaccessibleObjectException。
java.lang.reflect.InaccessibleObjectException:
Unable to make field private final java.lang.String java.lang.String.value
accessible: module java.base does not "opens java.lang" to unnamed module @...
处理顺序应该是:升级库 → 替换内部 API → 最后才用 --add-opens 作为临时兼容手段。
# 临时兼容:把 java.base/java.lang 打开给 classpath 上的代码
java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar
# 只导出、不打开(适合编译期访问公共但未导出的 API)
java --add-exports java.base/sun.nio.ch=ALL-UNNAMED -jar app.jar
--add-opens是运行时参数,不会改变代码的编译期可见性。ALL-UNNAMED指 classpath 上的未命名模块;模块化应用需要写具体模块名。- 每加一个
--add-opens,都应该记录原因和移除计划;它们会掩盖真正的升级问题。 jdeps --jdk-internals能列出静态引用;反射访问需要结合运行日志和库文档排查。
--add-opens java.base/java.lang=ALL-UNNAMED 了事。打开的包越多,未来升级越难,也削弱了 JDK 的封装保护。清理 JVM 参数与 GC 日志05
JVM 参数是升级时最容易「一启动就失败」的部分。下面的行为都在本机 JDK 21 上实际验证过。
| 旧参数 | JDK 21 行为 | 处理方式 |
|---|---|---|
-XX:+UseConcMarkSweepGC | Unrecognized VM option,JVM 无法启动 | 删除;改用 G1(默认)或 ZGC |
-XX:MaxPermSize=256m | Unrecognized VM option,JVM 无法启动 | 删除;PermGen 在 JDK 8 已移除 |
-XX:+UseBiasedLocking | Unrecognized VM option,JVM 无法启动 | 删除;偏向锁已不再作为可用选项 |
-XX:+PrintGCDetails | 警告并自动改用 -Xlog:gc* | 显式改成 -Xlog:gc* |
--illegal-access=permit | 忽略并警告,支持在 17 已移除 | 删除;用 --add-opens 处理具体访问 |
# JDK 21 推荐的 GC 日志写法
java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar
# 查看最终生效的 GC
java -XX:+PrintFlagsFinal -version | findstr UseG1GC
# Linux / macOS: java -XX:+PrintFlagsFinal -version | grep UseG1GC
删除 CMS 参数后,默认使用 G1。对暂停时间敏感且堆较大的服务,再评估 ZGC;不要因为「ZGC 更新」就默认它吞吐更高。
java -jar 改掉,线上仍会启动失败。被移除或降级的 Java API06
| API | JDK 21 行为 | 替代方案 |
|---|---|---|
Thread.stop() / suspend() / resume() | 抛 UnsupportedOperationException(JDK 17 仍可调用) | 用 interrupt()、volatile 标志、结构化取消 |
System.setSecurityManager(...) | 默认抛 UOE;-Djava.security.manager=allow 可临时启用 | 移除 Security Manager;JDK 24 起永久禁用 |
Object.finalize() | 仍可用,但已弃用(JDK 18) | try-with-resources、Cleaner、显式 close |
Runtime.runFinalizersOnExit | 早已移除 | 不要依赖 finalizer 执行时机 |
Thread.countStackFrames() | 抛 UnsupportedOperationException(方法仍在,但不再工作) | 用 Thread.getStackTrace() / StackWalker |
// 本机 JDK 21 实测
new Thread(() -> {}).stop();
// UnsupportedOperationException
System.setSecurityManager(new SecurityManager());
// UnsupportedOperationException:
// The Security Manager is deprecated and will be removed in a future release
# 编译期把「将被删除」的 API 调用暴露出来
javac --release 21 -Xlint:removal -d out $(find src -name "*.java")
Thread.stop 从来都不是安全的取消方式:它会释放所有监视器,让共享状态处于不一致状态。升级时顺手把它改成协作式取消,是收益最高的清理之一。预览特性:版本锁死,不能跨版本07
JDK 21 的预览特性(FFM、Scoped Values、结构化并发、String Templates)必须用 --enable-preview 编译,并且 class 文件会带上 65535 的 preview minor version。这样的 class 只能在同一个 JDK 版本上、并且运行时也加 --enable-preview 才能加载。
# 编译
javac --release 21 --enable-preview -d out App.java
# 运行(版本必须完全一致)
java --enable-preview -cp out App
# 只编译不运行,或用普通 java 启动:
UnsupportedClassVersionError: Preview features are not enabled for App
(class file version 65.65535). Try running with '--enable-preview'
# 把 JDK 21 预览 class 放到 JDK 25 上:
UnsupportedClassVersionError: App (class file version 65.65535)
was compiled with preview features that are unsupported. This version of the
Java Runtime only recognizes preview features for class file version 69.65535
- 预览特性不是「beta 版正式 API」:它可能在下一个版本改名、改签名,甚至被撤回。
- String Templates 在 21 预览、23 撤回;Scoped Values 的静态便捷方法在后续预览中被移除。
- 预览代码应隔离在适配层后面,不要把预览类型暴露到公共 API。
- 生产环境通常不应依赖预览特性;如果必须用,锁定 JDK 版本并把升级成本计入计划。
--enable-preview 不是「打开所有新功能」的开关。它只对当前 JDK 版本有效,并且要求编译期与运行期版本完全匹配。升级检查清单08
- 盘点所有运行时:本地、CI、容器、Kubernetes、构建插件,统一到目标 JDK。
- 用
--release 21编译,并开启-Xlint:removal;先让编译警告可见。 - 跑
jdeps --jdk-internals与jdeprscan --release 21 --for-removal,列出内部 API 和将被删除 API。 - 清理 JVM 参数:CMS、PermSize、BiasedLocking、旧的 GC 日志参数。
- 把
javax.*换成jakarta.*或显式依赖,升级脚本引擎与字节码库。 - 升级测试/监控 agent,处理 JDK 21 的动态加载 agent 警告(JEP 451)。
- 遇到反射错误,先升级库;确需兼容时才加最小范围的
--add-opens,并记录移除计划。 - 跑完整测试与压测,重点观察 GC 日志、启动时间、依赖加载和虚拟线程 pinning。
迁移完成的标准不是「能启动」,而是「启动参数干净、没有依赖 JDK 内部 API、测试与压测通过、回滚路径明确」。
版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| Java EE / CORBA 模块 | 11 移除 | JEP 320;改用 Jakarta EE 实现。 |
| Nashorn | 15 移除 | 11 弃用,15 移除;改用 GraalJS 等。 |
| CMS GC | 14 移除 | JDK 21 传入相关参数会导致 JVM 无法启动。 |
| 强封装 JDK 内部 | 16 默认 / 17 强化 | JEP 396 / 403;--illegal-access 在 17 移除。 |
| Thread.stop / suspend / resume | 21 实测 UOE | JDK 17 仍可调用;21 起抛 UnsupportedOperationException。 |
| Security Manager | 17 弃用 / 24 永久禁用 | 21 默认禁用,可用 allow 参数临时启用。 |
| 动态加载 agent 警告 | 21 警告 | JEP 451;启动时 -javaagent 不受影响。 |
| 预览特性 | 版本锁死 | class 文件 minor=65535,只能在同一 JDK 版本加 --enable-preview 运行。 |
| --release | 9 起提供 | JDK 21 最低支持 --release 8。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。