JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · MIGRATION · 8 / 11 / 17 → 21

从 8 / 11 / 17 迁移到 21

JDK 21 · 速查主题 12 / 12 · 预计 28 分钟
进阶28 分钟JEP 320JEP 403JEP 451
一句话升级 JDK 21 的难点通常不在新语法,而在旧假设:被移除的 JVM 参数、Java EE 模块、强封装下的反射,以及预览特性对版本和启动参数的严格要求。先处理这些,再谈用虚拟线程。

学完你要能自己回答

  1. 从 JDK 8 升到 21,哪些 Java EE 模块与 GC 参数必须先清理?
  2. --release 与 -source / -target 的区别是什么?为什么升级时优先用 --release 21?
  3. 强封装之后出现 InaccessibleObjectException,应该怎么定位和处理?
  4. 预览特性为什么不能跨版本运行?编译和运行各需要什么参数?

先确认版本坐标01

迁移的第一步不是改代码,而是把「源码、编译工具、运行时、依赖」四者的版本对齐。class 文件版本不匹配,是最常见的启动失败原因。

版本class 文件默认 GC迁移时最值得注意的变化
JDK 852ParallelPermGen 已移除;Java EE 模块仍在
JDK 1155G1Java EE / CORBA 移除;Nashorn 弃用
JDK 1761G1强封装 JDK 内部;RMI Activation 移除;Security Manager 弃用
JDK 2165G1动态加载 agent 警告;32 位 Windows 弃用;预览特性密集
JDK 2569G1Scoped 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。

升级顺序通常是 8 → 11 → 17 → 21,每个 LTS 作为检查点。直接 8 → 21 并非不可能,但会把多个版本的移除项、反射限制和依赖升级叠在一起,定位成本更高。

用 --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 的报告提升为编译警告。
CI 里至少保留一条「用目标 JDK 编译 + 用目标 JDK 运行」的任务。只在本机 IDE 里切换 JDK,很容易漏掉运行期参数与依赖差异。

Java EE 模块、Nashorn 与依赖升级03

JDK 11 通过 JEP 320 移除了 Java EE 与 CORBA 模块。很多老项目在 JDK 8 上能跑,是因为这些 API 还在 JDK 里;换到 11+ 后,必须显式引入第三方实现。

被移除的 API / 模块移除版本替代方案
javax.xml.bind(JAXB)11jakarta.xml.bind-api + 实现(如 GlassFish JAXB)
javax.xml.ws(JAX-WS)11jakarta.xml.ws-api + CXF / Metro
javax.activation11jakarta.activation-api + 实现
javax.annotation(Common Annotations)11jakarta.annotation-api
CORBA / org.omg.*11没有等价内置替代;迁移到其他 RPC / 消息方案
Nashorn JavaScript15GraalJS 等外部脚本引擎
# 找出依赖里还在引用 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,最后跑集成测试。

Nashorn 在 JDK 11 只是弃用,JDK 15 才移除。如果代码里还有 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:+UseConcMarkSweepGCUnrecognized VM option,JVM 无法启动删除;改用 G1(默认)或 ZGC
-XX:MaxPermSize=256mUnrecognized VM option,JVM 无法启动删除;PermGen 在 JDK 8 已移除
-XX:+UseBiasedLockingUnrecognized 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 更新」就默认它吞吐更高。

参数清理要覆盖所有入口:启动脚本、Dockerfile、Kubernetes args、IDE 运行配置、测试容器。只在本地 java -jar 改掉,线上仍会启动失败。

被移除或降级的 Java API06

APIJDK 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

  1. 盘点所有运行时:本地、CI、容器、Kubernetes、构建插件,统一到目标 JDK。
  2. 用 --release 21 编译,并开启 -Xlint:removal;先让编译警告可见。
  3. 跑 jdeps --jdk-internals 与 jdeprscan --release 21 --for-removal,列出内部 API 和将被删除 API。
  4. 清理 JVM 参数:CMS、PermSize、BiasedLocking、旧的 GC 日志参数。
  5. 把 javax.* 换成 jakarta.* 或显式依赖,升级脚本引擎与字节码库。
  6. 升级测试/监控 agent,处理 JDK 21 的动态加载 agent 警告(JEP 451)。
  7. 遇到反射错误,先升级库;确需兼容时才加最小范围的 --add-opens,并记录移除计划。
  8. 跑完整测试与压测,重点观察 GC 日志、启动时间、依赖加载和虚拟线程 pinning。

迁移完成的标准不是「能启动」,而是「启动参数干净、没有依赖 JDK 内部 API、测试与压测通过、回滚路径明确」。

升级到 21 不等于必须立刻使用虚拟线程。先把运行时和依赖升稳,再针对阻塞 I/O 场景逐步引入虚拟线程,风险更可控。

版本边界BOUNDARY

这张表比 API 本身更值得记。写代码时先确认你的运行时版本。

特性 / API状态说明
Java EE / CORBA 模块11 移除JEP 320;改用 Jakarta EE 实现。
Nashorn15 移除11 弃用,15 移除;改用 GraalJS 等。
CMS GC14 移除JDK 21 传入相关参数会导致 JVM 无法启动。
强封装 JDK 内部16 默认 / 17 强化JEP 396 / 403;--illegal-access 在 17 移除。
Thread.stop / suspend / resume21 实测 UOEJDK 17 仍可调用;21 起抛 UnsupportedOperationException。
Security Manager17 弃用 / 24 永久禁用21 默认禁用,可用 allow 参数临时启用。
动态加载 agent 警告21 警告JEP 451;启动时 -javaagent 不受影响。
预览特性版本锁死class 文件 minor=65535,只能在同一 JDK 版本加 --enable-preview 运行。
--release9 起提供JDK 21 最低支持 --release 8。

费曼自检SELF CHECK

先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。

01. 为什么升级时优先用 --release 21,而不是 -source 21 -target 21?
--release 会同时约束语法、字节码版本和可用的标准库 API 快照(ct.sym),能阻止你误用更高版本的 API;-source/-target 只改变语法与 class 版本,运行时仍可能因缺少 API 而失败。
02. 从 JDK 8 升到 21,JAXB 相关代码为什么编译不过?
JAXB 等 Java EE 模块在 JDK 11 已被 JEP 320 移除。需要显式引入 jakarta.xml.bind-api 及实现,并把 javax.xml.bind 的 import 迁移到 jakarta.xml.bind。
03. 出现 InaccessibleObjectException 时,应该先加 --add-opens 吗?
不应该。先确认是哪个库在反射访问 JDK 内部包,优先升级或替换该库;--add-opens 只应作为临时兼容手段,并限定到最小包和模块。
04. 为什么 JDK 21 编译的预览 class 不能放到 JDK 25 上运行?
预览 class 的 minor version 是 65535,且与编译时的 JDK 版本绑定。JDK 25 只识别 69.65535,不识别 65.65535,因此会抛 UnsupportedClassVersionError。
05. Thread.stop() 在 JDK 21 上还能用吗?
不能。JDK 21 实测会抛 UnsupportedOperationException;JDK 17 仍可调用但有弃用警告。应改用 interrupt、volatile 标志或结构化取消。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。