适用读者:要做"升不升 JDK"决策的人;要答"Java 17 有什么新东西"的面试者。
前置:S04-A01(版本演进与团队基线)、J11。
覆盖旧文:2769(Java 8)+3013、2770(Java 17)+3014、2771(Java 21)+3015→ 6 篇 / 约 4.3 万字符。
扩展点:E-S04-036(版本特性矩阵:8/11/17/21 的关键差异)、E-S04-037(升级收益/代价决策表)、E-S04-038(虚拟线程的适用边界)。
上一篇:S04-J11 · 反射、注解、泛型、序列化:运行时的四把钥匙
下一篇:S04-X02 · 技巧、错误总结与片段登记。
1. 一句话结论
升级 JDK 不是一个技术决定,是一个"收益 vs 迁移成本"的财务决定。 收益侧是 GC/启动/语法;成本侧是依赖兼容、--add-opens、废弃 API、测试回归。没有任何收益适合"顺手升"。
出处:
A-2769–A-2771、A-3013–A-3015。
2. 版本特性矩阵(E-S04-036)
| 版本 | LTS | 语言 | 库/平台 | 运行时 |
|---|---|---|---|---|
| 8 | ✅ | Lambda、Stream、Optional、default 方法 |
新 java.time |
元空间取代永久代 |
| 9 | — | (无大语法) | 模块系统 JPMS、集合工厂 List.of |
G1 成默认 |
| 10 | — | var 局部变量推断 |
— | — |
| 11 | ✅ | var in lambda 参数 |
String.isBlank/strip/repeat、Files.readString、HTTP Client 标准化 |
ZGC 实验性、G1 优化 |
| 14 | — | switch 表达式(预览)、record(预览) |
— | CMS 移除 |
| 15 | — | 文本块 | — | 偏向锁默认禁用 |
| 16 | — | record 正式、instanceof 模式匹配 |
— | — |
| 17 | ✅ | sealed 类、switch 模式匹配(预览) |
强封装 JDK 内部(--add-opens 需求爆发) |
ZGC 生产可用 |
| 21 | ✅ | switch 模式匹配正式、record 模式、虚拟线程、SequencedCollection |
分代 ZGC | 虚拟线程(JEP 444) |
必答三点:
var只是语法糖(编译期推断,不是动态类型),不能用于字段/方法参数/返回值。record是"透明数据载体":自动equals/hashCode/toString,字段final,不能继承类(可实现接口)。sealed让"穷尽性检查"成为可能:sealed interface Shape permits Circle, Square+switch模式匹配编译器可校验分支完整。
3. 升级决策表(E-S04-037)
| 决策维度 | 8 → 11 | 11 → 17 | 17 → 21 |
|---|---|---|---|
| 主要收益 | HTTP Client、String/Files 便利方法、G1 改进 |
强封装(更安全)、record/sealed、ZGC 可用 |
虚拟线程(IO 密集吞吐)、分代 ZGC(低停顿) |
| 主要成本 | 少数依赖不兼容、Jigsaw 前期踩坑 | --add-opens 大面积出现(反射库、序列化库) |
虚拟线程需改造(synchronized pinning) |
| 风险点 | 反射内部 API 报警告 | 框架版本必须同步升(Spring Boot 3 需 17+) | 线程池模型与虚拟线程混用的调试复杂度 |
| 不升的理由 | 生态未验证、客户环境锁定 | 依赖链太长、缺少回归测试 | 现有吞吐已达标、无低延迟诉求 |
判据(架构师口径):升级的最小充分条件 = "有明确指标需要它"——例如"GC 停顿 P99 > 500 ms"→ 考虑 G1/ZGC;"IO 密集服务线程数打满"→ 考虑虚拟线程。没有指标,不升级。
4. 虚拟线程的适用边界(E-S04-038)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> { Thread.sleep(1000); return i; }));
} // JDK 21;约 1 秒完成 1 万任务(IO 等待被复用)
| 维度 | 平台线程 | 虚拟线程(21) |
|---|---|---|
| 栈 | 固定 1 MB(默认) | 按需增长,KB 级 |
| 调度 | 内核 1:1 | JVM 调度到载体线程(ForkJoinPool) |
| 阻塞 | 占内核线程 | IO/sleep 卸载,不占载体 |
| pinning | — | synchronized 块内阻塞会钉住载体线程(21 限制) |
| 适合 | CPU 密集 | IO 密集、高并发短任务 |
三条边界(必答):
① CPU 密集任务不会因虚拟线程变快(并行度 = 核数)
② synchronized 内做阻塞 IO 会 pin 载体线程 → 用 ReentrantLock 替代
③ 不要池化虚拟线程(它本就轻量,池化反而失去意义)
待核:JDK 24 起对 synchronized pinning 有改进(JEP 491),本文按 JDK 21 口径写。
5. 迁移执行清单
| 阶段 | 动作 | 验收 |
|---|---|---|
| 1 评估 | 依赖树扫描(jdeps/mvn dependency:tree),列出反射内部 API 使用点 |
生成 --add-opens 候选清单 |
| 2 构建 | 升级编译插件 target/release,开 -Xlint:deprecation |
编译零警告(或登记豁免) |
| 3 运行 | 灰度节点跑新 JDK,对比 P99、GC 停顿、错误率 | 三项指标不劣化 |
| 4 回滚 | 保留旧 JDK 镜像与启动脚本 | 5 分钟内可回切 |
| 5 固化 | CI 固定 JDK 版本(toolchain),写进 README |
新成员环境一致 |
# 快速定位内部 API 依赖(JDK 17+)
jdeps --jdk-internals -R app.jar
6. 反例:五种"升级踩坑"的典型
| 做法 | 后果 |
|---|---|
| 只升运行时、不升框架 | Spring Boot 2.x 在 17 上能跑但不受支持;3.x 强制 17+ |
全量 --add-opens java.base |
等于放弃强封装收益,且掩盖真实依赖问题 |
| 把虚拟线程塞进固定线程池 | 失去调度优势,还可能造成 pinning 放大 |
用 var 替代所有类型 |
可读性下降,代码评审成本上升 |
| 升级后不改 CI JDK | 本地 21、CI 8,产物在线上行为不同 |
7. 自测
var与动态类型的区别?哪些位置不能用?record的三个自动方法与两条限制?sealed解决的问题是什么?- 虚拟线程的三条边界?为什么不能池化?
- 升级决策的最小充分条件?迁移五阶段?
答案
- 编译期推断的静态类型;字段/参数/返回值/局部未初始化不可用。
equals/hashCode/toString;字段 final 且不能继承类(可实现接口)。- 让类型集合封闭,编译器可做穷尽性检查(配合 switch 模式匹配)。
- CPU 密集无收益、
synchronized阻塞 pin 载体、不要池化;池化失去轻量意义。 - 有明确指标需要它;评估→构建→灰度运行→回滚→固化。
8. 下一篇
S04-X02 · 技巧、错误总结与片段登记 —— 正则、日志与调试、单元测试、设计模式、关键词大全等 9 篇的登记表与去向。
本篇属于系列《S04 · Java 语言核心》。旧文来源:A-2769/2770/2771、A-3013/3014/3015;扩展点 E-S04-036/037/038。