升级 JDK 就像给汽车换发动机------看着简单,拆开全是线。这份清单帮你提前"排雷"。
一、升级前的通用准备(所有版本通用)
无论你从哪个版本开始升级,以下准备工作是必修课:
1.1 依赖梳理
bash
# 导出完整的依赖树,排查第三方库兼容性
mvn dependency:tree -Dverbose > deps.txt
重点关注以下依赖的版本是否支持目标 JDK:
|---------------------|-----------------|-----------------|-----------------|
| 依赖 | JDK 17 最低版本 | JDK 21 最低版本 | JDK 25 最低版本 |
| Maven | 3.6.3+ | 3.8.8+ | 3.9.6+ |
| Gradle | 7.3+ | 8.0+ | 8.5+ |
| Spring Boot | 2.7.x / 3.x | 3.1+ | 3.4+ / 4.0 |
| Spring Cloud | 2021.0.x+ | 2022.0.x+ | 2024.0.x+ |
| MyBatis-Plus | 3.5.3.1+ | 3.5.5+ | 3.5.7+ |
| MySQL Connector | 8.0.25+ | 8.0.33+ | 8.0.35+ / 9.x |
| Netty | 4.1.68+ | 4.1.94+ | 4.1.100+ |
| Hibernate | 5.6+ / 6.x | 6.2+ | 6.4+ |
1.2 代码扫描
# 检测对 JDK 内部 API 的依赖(必做!)
jdeps --jdk-internals -R your-app.jar
# 检测废弃 API
jdeprscan your-app.jar
1.3 通用 JVM 参数调整
|----------------------------|--------------------------------|---------------------|
| 旧参数(JDK 8/11) | 新参数(JDK 17+) | 说明 |
| -XX:+PrintGCDetails | -Xlog:gc* | 日志系统统一 |
| -XX:+PrintGCTimeStamps | -Xlog:gc* | 同上 |
| -XX:+PrintGCDateStamps | -Xlog:gc* | 同上 |
| -XX:+UseParallelGC | -XX:+UseG1GC 或 -XX:+UseZGC | 默认 GC 已变 |
| -XX:MaxMetaspaceSize | 保留可用 | 元空间管理优化 |
| -XX:+UseContainerSupport | JDK 10+ 默认开启 | 容器感知 |
| --illegal-access=permit | JDK 17 已删除 | 必须用 --add-opens替代 |
1.4 灰度发布规范
|--------|-------------|--------------------|
| 阶段 | 操作 | 验证指标 |
| 阶段 1 | 本地开发环境改造 | 编译通过、单元测试通过 |
| 阶段 2 | 测试环境全链路压测 | P99 延迟、GC 停顿、内存无泄漏 |
| 阶段 3 | 线上灰度 10% 流量 | 稳定运行 24~72 小时 |
| 阶段 4 | 逐步放量至 100% | 监控无告警 |
回滚条件(满足任意一条立即回滚):
- P99 接口延迟上涨超过 50%
- GC STW 持续超过 200ms
- 出现反射 / 模块访问异常
- 容器被 OOM Kill 频率上升
二、JDK 7 → JDK 8 迁移
难度 :⭐⭐ | 推荐路径:JDK 7 → JDK 8(直接升级)
⚠️ 主要变更
|----------------------------|-------------------------------------|------------------------------------------------|
| 变更项 | 说明 | 解决方案 |
| PermGen 被 Metaspace 替代 | -XX:PermSize、-XX:MaxPermSize 失效 | 替换为 -XX:MetaspaceSize、-XX:MaxMetaspaceSize |
| Nashorn JS 引擎替代 Rhino | javax.script 实现变更 | 代码无需改动,但注意 JS 兼容性 |
| 日期 API 迁移 | Date/Calendar 仍可用但推荐替换 | 新代码直接用 java.time |
✅ 推荐操作
- 移除 JVM 参数中的
-XX:PermSize和-XX:MaxPermSize - 新代码全面使用
java.time替代Date/Calendar - 尝试在代码中引入 Lambda 和 Stream 简化逻辑(非强制)
三、JDK 8 → JDK 11 迁移
难度 :⭐⭐⭐ | 推荐路径:JDK 8 → JDK 11(建议先到 11 再过渡到更高版本)
⚠️ 主要变更
|-------------------------------------|-----------------------|------------------------|
| 变更项 | 说明 | 解决方案 |
| 模块化系统 JPMS 引入 | JDK 内部 API 不再默认开放 | 添加 --add-opens启动参数 |
| javax.xml.bind (JAXB) 移除 | 不再随 JDK 提供 | 手动添加 Maven 依赖 |
| javax.xml.ws (JAX-WS) 移除 | 同上 | 手动添加 Maven 依赖 |
| javax.corba 移除 | CORBA 相关 API 完全删除 | 迁移或移除相关功能 |
| sun.misc.BASE64Encoder 移除 | 编译报错 | 替换为 java.util.Base64 |
| sun.misc.Unsafe 受限 | 部分操作受限 | 使用 VarHandle替代 |
| JavaFX 分离 | 不再随 JDK 分发 | 单独引入 OpenJFX 模块 |
| 默认 GC 改为 G1 | JDK 8 默认是 Parallel GC | 评估 GC 行为变化,调整参数 |
🔧 常用 --add-opens参数
bash
# 如果遇到反射访问受限,添加以下参数(按需组合)
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
--add-opens java.base/sun.security.provider=ALL-UNNAMED
📦 缺失依赖添加
XML
<!-- JAXB -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.1</version>
</dependency>
<!-- JAX-WS -->
<dependency>
<groupId>javax.xml.ws</groupId>
<artifactId>jaxws-api</artifactId>
<version>2.3.1</version>
</dependency>
✏️ 代码修改示例
java
// ❌ JDK 8 写法(JDK 11 编译报错)
import sun.misc.BASE64Encoder;
String encoded = new BASE64Encoder().encode("Hello".getBytes());
// ✅ JDK 11 写法
import java.util.Base64;
String encoded = Base64.getEncoder().encodeToString("Hello".getBytes());
四、JDK 8 → JDK 17 迁移(重点!跨度最大)
难度 :⭐⭐⭐⭐ | 推荐路径:JDK 8 → JDK 17(推荐直接升级,跳过 JDK 11)
⚠️ JDK 8 到 17 的核心雷区
|---------------------------|----------------------------|-------------------------------|
| 类别 | 具体问题 | 解决方案 |
| API 移除 | sun.misc.* 全部不可访问 | 替换为标准 API |
| 模块系统 | JPMS 强封装,反射受限 | 添加 --add-opens或升级框架 |
| 反射限制 | setAccessible(true) 部分失效 | 使用 MethodHandles或添加模块权限 |
| GC 变化 | 默认 G1,ZGC 可用 | 重新评估 GC 参数配置 |
| 容器适配 | 旧版本无容器感知 | 开启 -XX:+UseContainerSupport |
| 安全管理器 | SecurityManager 废弃 | 迁移安全策略 |
| Applet API | 已删除 | 移除相关依赖 |
| AOT/JIT 编译器 | 实验性 Graal JIT 移除 | 改用 GraalVM 独立方案 |
| -XX:+AggressiveOpts | 已删除 | 移除该参数 |
🔧 Maven 配置更新
XML
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>--release</arg>
<arg>17</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
✏️ 常见代码迁移对照表
|-----------------------------------------------------------|---------------------------------------------|-----------------|
| JDK 8 写法 | JDK 17 写法 | 说明 |
| new BASE64Encoder().encode(bytes) | Base64.getEncoder().encodeToString(bytes) | BASE64 编码 |
| sun.misc.Unsafe.allocateInstance() | Constructor.newInstance() 或 VarHandle | 对象实例化 |
| if (obj instanceof String) { String s = (String) obj; } | if (obj instanceof String s) { ... } | instanceof 模式匹配 |
| 手写 POJO + getter/setter | public record User(...) {} | Record 替代 |
| String json = "{\n \"name\": \"a\"\n}" | String json = """ { "name": "a" } """ | 文本块 |
| switch(x) { case 1: break; ... } | switch(x) { case 1 -> ... } | Switch 表达式 |
五、JDK 11 → JDK 17 迁移
难度 :⭐⭐ | 推荐路径:JDK 11 → JDK 17(相对平滑)
⚠️ 主要变更
|-------------------------|--------------------------------|----------------------------------|
| 变更项 | 说明 | 解决方案 |
| 强封装内部 API | --illegal-access=permit 不再可用 | 必须用 --add-opens显式开放 |
| SecurityManager 废弃 | 标记为废弃待移除 | 迁移安全策略 |
| finalize() 废弃 | 标记为废弃 | 改用 Cleaner或 PhantomReference |
| RMI Activation 移除 | 已删除 | 移除相关依赖 |
| Applet API 废弃 | 已废弃 | 移除相关依赖 |
| GC 选项 | ZGC/Shenandoah 转正 | 可选启用 |
✅ 核心操作
- 移除 JVM 参数中的
--illegal-access=permit - 检查反射代码,替换为
--add-opens或MethodHandles - 将
finalize()方法迁移到Cleaner机制 - 考虑启用 ZGC(低延迟场景)或 Shenandoah(ARM 场景)
六、JDK 17 → JDK 21 迁移
难度 :⭐⭐ | 推荐路径:JDK 17 → JDK 21(升级体验最好的跳段)
⚠️ 主要变更
|------------------------------|------------------------------|-----------------------------------|
| 变更项 | 说明 | 解决方案 |
| 虚拟线程正式转正 | Thread.ofVirtual() 可用 | 可引入但无需强制修改现有代码 |
| String Templates(预览) | STR."Hello \{name}" | 预览特性,生产慎用 |
| finalize() 正式废弃待移除 | 编译警告 | 迁移到 Cleaner |
| 分代 ZGC 可用 | 默认不分代,需手动开启 | -XX:+UseZGC -XX:+ZGenerational |
| synchronized Pinning 问题 | 虚拟线程中使用 synchronized会被"钉死" | 建议用 ReentrantLock替代(JDK 25 已修复) |
⚠️ 虚拟线程使用注意事项
java
// ✅ 正确用法:IO 密集型场景
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> handleIoRequest());
}
// ❌ 避免:不要在虚拟线程中使用 synchronized 块(JDK 21 版本)
synchronized (lock) { // 会导致 Pinning,并发能力暴跌
// ...
}
// ✅ 改用 ReentrantLock
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
// ❌ 避免:不要池化虚拟线程(虚拟线程不是池化资源)
// ❌ Executors.newFixedThreadPool(100) 不对
// ✅ 每次任务新建虚拟线程
🔧 GC 配置建议
# 低延迟场景(推荐)
-XX:+UseZGC -XX:+ZGenerational -Xms4g -Xmx4g
# 通用场景
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
七、JDK 21 → JDK 25 迁移
难度 :⭐ | 推荐路径:JDK 21 → JDK 25(最顺滑的升级)
⚠️ 主要变更
|-----------------------------|-------------------------|-------------|
| 变更项 | 说明 | 解决方案 |
| ScopedValues 正式转正 | 替代 ThreadLocal | 新代码优先使用 |
| synchronized Pinning 修复 | 虚拟线程中可安全使用 synchronized | 代码无需修改 |
| 紧凑对象头默认开启 | 对象头 16B→8B,省内存 | 无需操作,默认生效 |
| 分代 ZGC 默认开启 | 吞吐量大幅提升 | 无需操作 |
| 分代 Shenandoah 转正 | 新增分代模式 | 按需启用 |
| 32 位 x86 支持移除 | 不再支持 32 位系统 | 确保使用 64 位系统 |
| 紧凑源文件 + 实例 main | void main() {} 替代传统写法 | 新代码可用 |
| 灵活构造函数体 | super() 前可写校验逻辑 | 新代码可用 |
| 后量子加密 KDF API | 新增密钥派生函数 API | 按需使用 |
✅ 核心操作
- 几乎无需修改现有代码------JDK 21 到 25 的兼容性极好
- 之前为了规避 Pinning 而改写的
ReentrantLock,可以改回synchronized(但不强制) - 评估是否将
ThreadLocal替换为ScopedValues(新模块推荐,存量模块可选) - 移除 JVM 参数中手动开启分代 ZGC 的参数(JDK 25 默认已开启)
- 享受紧凑对象头带来的内存红利(约 15%~22% 的内存节省)
✏️ 新特性示例代码
java
// ScopedValues 替代 ThreadLocal
private static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
void handleRequest() {
ScopedValue.where(TRACE_ID, "trace-123").run(() -> {
// 整个作用域内都能获取到 TRACE_ID
String id = TRACE_ID.get();
process(id);
}); // 自动释放,绝不泄漏
}
// 实例 main 方法
// Hello.java
void main() {
System.out.println("Hello, JDK 25!");
}
// 灵活构造函数体
class User {
User(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("名称不能为空");
} // ✅ 在 super() 前做校验
super();
}
}
八、一键速查:各升级路径对比
|-----------------|--------|---------------------------|---------|---------------|
| 升级路径 | 难度 | 主要工作量 | 推荐度 | 建议策略 |
| JDK 7 → 8 | ⭐⭐ | 低,主要是 PermGen 参数调整 | ✅ | 直接升,7 太老了 |
| JDK 8 → 11 | ⭐⭐⭐ | 中,需处理模块化、移除 API | ✅ | 可作为过渡 |
| JDK 8 → 17 | ⭐⭐⭐⭐ | 高,需一次性处理模块+反射+GC+依赖 | ✅✅✅ | 推荐直接升 17 |
| JDK 11 → 17 | ⭐⭐ | 低,改动量小 | ✅✅ | 推荐 |
| JDK 11 → 21 | ⭐⭐⭐ | 中,可享受虚拟线程 | ✅✅ | 推荐 |
| JDK 17 → 21 | ⭐⭐ | 低,注意 synchronized Pinning | ✅✅✅ | 强烈推荐 |
| JDK 17 → 25 | ⭐⭐ | 低,改动量极小 | ✅✅✅ | 强烈推荐,一步到位 |
| JDK 21 → 25 | ⭐ | 极低,几乎无痛升级 | ✅✅✅✅ | 最顺滑的升级 |
九、最后的一些"血泪教训"
- 不要直接在生产环境换 JDK 版本------先走灰度,哪怕你觉得"应该没问题"
- 工具链版本要一起升------Maven、Gradle、IDEA 都得跟上,否则编译报错会让你怀疑人生
--add-opens是过渡方案,不是长期方案------依赖它的第三方库尽快升级到兼容版本- GC 参数不是玄学------换了 JDK 版本一定要做 GC 压测,同样的参数在不同 JDK 上表现可能完全不同
- 虚拟线程不是万能药------它适合 IO 密集型,不适合 CPU 密集型
- 32 位已死,有事烧纸------JDK 25 彻底移除了 32 位 x86 支持,还在用 32 位系统的赶紧升级
一句话总结 :JDK 8 → 17 跨度最大但最值得,JDK 17 → 21 → 25 升级一次比一次顺滑。早升早享受,晚升......也不是不行,但安全补丁可不等人。