前三篇把 JVM 内存与类加载、垃圾回收、JIT 与调优讲透了。落到工程上还有一个绕不开的问题:线上钉在哪个 JDK?从 8 升到 17 / 21 / 25,语言、运行时和升级成本到底变了什么?
本篇不以完整 JEP 清单铺开,而以 LTS(Long-Term Support) 为主线,把真正影响写法、默认行为与迁移成本的关键差异讲清楚,并给出可落地的选型建议。
TL;DR
- 生产跟 LTS:主线是 8 / 11 / 17 / 21 / 25;9 起每 6 个月一版,21 起 LTS 约两年一档;非 LTS 适合尝鲜,不适合当长期基线。
- 运行时三连:① 8:PermGen → Metaspace;② 9+:默认 GC → G1(CMS 于 14 移除);③ 21:虚拟线程正式 + 分代 ZGC。
- 语言主线:8 Lambda/Stream → 11
var/HttpClient/模块落地 → 17 Records/Sealed/Pattern Matching/Text Blocks → 21 虚拟线程 + patternswitch→ 25 Scoped Values 正式。 - 虚拟线程要点:轻量、挂在少量 carrier(载体)平台线程 上;阻塞 I/O 时可 unmount;
synchronized/ native 可能 pinning —— 不是「无脑比线程池快」。 - 升级高频坑:Java EE 模块移除、内部 API 强封装、
-XX:+PrintGCDetails失效、仍开着 CMS 会直接起不来。
Table of contents
Open Table of contents
1. 先搞清:什么是 LTS
JDK 9 之后,Oracle / OpenJDK 采用 每 6 个月发一版 的节奏。其中少数版本标为 LTS:厂商提供更长周期的安全更新与补丁,生态(Spring、中间件、云镜像、监控 Agent)也优先对齐这些版本。
| 角色 | 版本 | 含义 |
|---|---|---|
| LTS | 8、11、17、21、25… | 生产基线;支持周期以年计 |
| 非 LTS(Feature Release) | 9–10、12–16、18–20、22–24… | 功能试验田;通常只维护到下一版发布 |
一句话:非 LTS 是「功能快递」,LTS 是「可长期寄居的房子」。团队缺专人跟版本演进时,钉 LTS 几乎总是更优解。
自 21 起 LTS 约两年一档;图中箭头标注相邻 LTS 的大致间隔。
绿 = 语言与标准库;蓝 = 运行时 / 性能;粉 = 移除或强约束 —— 升级时粉条最容易踩坑。
| 关键变化 | 哪版 | 一句话 |
|---|---|---|
| PermGen → Metaspace | 8 | 类元数据出堆、用本地内存,缓解 PermGen space OOM |
| 默认 GC | 9+ | Parallel → G1;CMS 在 14 删除 |
| 模块系统 JPMS | 9 / 11 LTS | JDK 自身模块化;应用可不写 module-info,但内部 API 渐难反射 |
| Lambda / Stream | 8 | 行为参数化 + 声明式集合流水线 |
| Records / Sealed / Pattern Matching | 16–17 | 数据载体、限制继承、instanceof 模式绑定 |
| 虚拟线程 | 21 | JVM 调度的轻量线程;I/O 密集高并发的现代答案 |
| Scoped Values | 25 正式 | 虚拟线程时代替代部分 ThreadLocal 的作用域共享 |
2. 运行时关键差异:Metaspace、G1、日志
语言特性看得见,默认 GC、方法区实现、日志开关却常在升级后才以事故形式出现。
2.1 PermGen → Metaspace(JDK 8)
| 永久代(≤7) | 元空间(8+) | |
|---|---|---|
| 存什么 | 类元信息、方法、常量池等 | 同类信息(字符串常量池 JDK 7 已先挪进堆) |
| 内存位置 | JVM 堆内一块固定/难扩的区域 | 本地内存(Native),默认可按需扩 |
| 典型故障 | java.lang.OutOfMemoryError: PermGen space | Metaspace(常因动态类太多:反射、CGLIB、热部署) |
| 调参 | -XX:PermSize / MaxPermSize | -XX:MetaspaceSize / MaxMetaspaceSize |
换的原因:永久代大小难估,加载类一多就 OOM;元空间用本地内存,默认更弹性。但不是「永远不 OOM」——动态生成类失控照样打满 Metaspace。对象仍在堆,类的「图纸」在元空间,对应内存篇的方法区实现变迁。
2.2 默认 GC:Parallel → G1,CMS 退场
- JDK 8:服务端常见默认是 Parallel GC(吞吐优先,STW 停顿相对明显)。
- JDK 9+:G1 成为默认 —— Region 化、可设停顿目标(
-XX:MaxGCPauseMillis),更适中大堆在线服务(机制见GC 篇)。 - CMS:并发老年代的开拓者,但碎片与维护成本高;JDK 9 废弃,JDK 14 移除。配置里若还写着
-XX:+UseConcMarkSweepGC,进程会直接起不来。 - 低延迟线:ZGC / Shenandoah;21 起 分代 ZGC,25 分代 Shenandoah —— 大堆且要稳定低停顿时再评估。
2.3 GC 日志:-XX:+PrintGCDetails → -Xlog:gc*
旧旗标(PrintGCDetails、PrintGCDateStamps 等)在统一日志后逐步失效或被替代。新写法:-Xlog:gc*(可再加 gc+heap、gc+age 等标签)。统一日志是 整套 JVM 日志框架,不只 GC。
3. 语言与 API:从 8 到 17
3.1 JDK 8:Lambda、Stream、接口默认方法
- Lambda:把「一段行为」当成参数传给方法(配合函数式接口)。底层靠
invokedynamic+ 方法句柄,由 LambdaMetafactory 在运行期生成调用位点,语义接近匿名内部类但实现不是简单的内部类语法糖。 - Stream:声明式数据处理(
filter/map/collect);中间操作惰性、终端操作触发;并行流并非总更快(任务要够大、且避免共享可变状态)。 - 接口
default/static方法:库可演化接口而不打碎所有实现;JDK 9 起还有接口private方法供默认方法复用。 java.time:替代易错的Date/Calendar;时区用ZoneId。CompletableFuture:异步编排入口(细节见 JUC 篇)。
顺带一提集合实现:JDK 8 起 HashMap 在链表过长(默认树化阈值 8,且容量 ≥64)时转为 红黑树,最坏查找从 O(n) 降到 O(log n);扩容时由头插改为尾插,降低并发场景下成环风险(HashMap 本身仍非线程安全,并发用 ConcurrentHashMap)。
3.2 JDK 11:var、HttpClient、模块化落地
var(自 10):局部变量类型推断 —— 声明处必须能推断,不是动态类型;字段、方法参数不能随便用var(以局部变量为准最稳)。- HttpClient:标准库 HTTP/1.1、HTTP/2、异步。
java Xxx.java单文件启动:小工具 / 脚本场景。
模块系统(JPMS,9 引入,11 作为 LTS 广泛落地)
- JDK 自身拆成模块,加强封装,减少「随便反射进
sun.*」。 - 应用 可以继续用 classpath(未模块化),但会越来越常碰到 非法反射访问 警告 / 错误。
- 关键词:
module-info.java、requires/exports、模块路径 vs 类路径。 - 升级痛点:JAXB 等 Java EE / CORBA 模块从 JDK 移除,要用就显式加依赖;
sun.misc.BASE64Encoder之类内部 API 不可再依赖。
3.3 JDK 17:Records、Sealed、Pattern Matching、Text Blocks
17 把 12–16 预览期沉淀的一批语言特性收成正式能力,也是 Spring Boot 3 等生态对齐的基线(要求 17+)。
// Records:不可变数据载体,自动 equals/hashCode/toString/accessor
record Point(int x, int y) {}
// Pattern Matching for instanceof(16 正式,17 生态标配)
if (obj instanceof String s) {
System.out.println(s.toLowerCase());
}
// Sealed:限制谁可以继承,配合 switch 做穷尽检查
sealed interface Shape permits Circle, Rect {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
- Record:语义是「数据透明载体」,默认 final、组件不可变;适合 DTO,不适合当随意可变实体。
- Sealed:把继承树关进白名单,模式匹配才能做 穷尽性 检查。
switch表达式:可返回值、箭头分支;到 21 再升级为 pattern switch(按类型 / record 解构匹配)。- Text Blocks(15 正式):
"""..."""多行字符串,拼 SQL / JSON / HTML 更清晰。
运行时侧:G1 默认地位稳固;强封装 JDK 内部 API 默认拒绝许多非法反射 —— 过去靠 --add-opens 苟着的库,到 17 往往会彻底暴露问题。
4. 并发分水岭:虚拟线程(JDK 21)
21 是近年来对服务端架构影响最大的 LTS —— 关键词是 Virtual Threads(虚拟线程,Project Loom)。
| 平台线程(Platform Thread) | 虚拟线程(Virtual Thread) | |
|---|---|---|
| 调度 | OS 调度 | JVM 调度 |
| 成本 | 栈大约 MB 级,几千到上万就吃紧 | 很轻,可到 百万级 并发任务 |
| 模型 | 线程池精心限流 | 可「一任务一线程」重新成立 |
| 擅长 | CPU 密集仍靠它 | 阻塞 I/O 密集(吞吐型并发) |
机制三词
- Carrier(载体线程):虚拟线程跑在少量平台线程上。
- Mount / Unmount:遇到阻塞 I/O,虚拟线程可从 carrier 卸下,carrier 去跑别的;I/O 完成后再挂上去。
- Pinning(钉住):若在虚拟线程里持有
synchronized监视器 或进入某些 native 帧,可能无法 unmount,carrier 被钉住 —— 吞吐回落。优先用ReentrantLock等,并避免在虚拟线程里堆重 CPU 或大线程局部状态。JDK 21–23 上可用-Djdk.tracePinnedThreads/ JFRVirtualThreadPinned观察;JDK 24(JEP 491) 起synchronized内阻塞不再因此 pinning。
// JDK 21:按任务创建虚拟线程 —— 适合大量阻塞调用
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> httpClient.send(request, BodyHandlers.ofString())));
}
虚拟线程让你用 同步写法 接近异步的伸缩性;不是取代 Netty 这类网络栈,而是减少「手写异步回调」的必要。CPU 密集仍要控制并行度,别幻想「开一百万虚拟线程算矩阵就更快」。它也 不会一刀切取代线程池:I/O 密集、高并发任务可用虚拟线程简化模型;需要严格限流、或与线程绑定昂贵资源时,平台线程池仍然合适。
21 同期值得记住的
- Generational ZGC:年轻对象分代回收,低延迟路线更敢用在通用服务上。
- Sequenced Collections:统一「有顺序」的集合抽象(
getFirst/reversed等)。 - Record Patterns / pattern switch:模式匹配能力补齐。
5. JDK 25:当前最新 LTS
25 于 2025-09-16 GA,是 21 之后的 LTS。相对 21,更突出 运行时效率、AOT 易用性、可观测性,以及若干 API 收官。
| 能力 | 含义 |
|---|---|
| Scoped Values(正式) | 作用域内只读共享,虚拟线程下比乱用不限的 ThreadLocal 更安全、语义更清晰 |
| Compact Object Headers | 压缩对象头,降低堆占用 —— 扣回内存篇对象头知识 |
| 分代 Shenandoah | 低延迟 GC 阵营与分代 ZGC 并列的可选项 |
| AOT / Leyden 易用性 | 改善启动与预热(对照 JIT 预热痛点) |
| Preview / Incubator | Structured Concurrency、Primitive Patterns、Vector API 等 尚未稳定,核心路径别赌 |
定位:新项目优先 21(生态最成熟的「现代 Java」);已在 21 且依赖支持,再评估 25。不要把 Preview 特性写进核心契约。
6. 升级路径与选型
稳字当头:先离开失控的旧基线,再按生态成熟度跃进。
| 现状 | 建议 |
|---|---|
| 仍在 8 | 尽快迁出;优先目标 17 或 21(视 Spring 等框架约束) |
| 停在 11 | 规划升 17/21;重点处理模块封装与 EE 依赖 |
| 已在 17 且稳定 | 可守;新服务用 21,存量择机跳 |
| 新项目 | 默认 21;团队愿跟最新 LTS 且依赖支持则评估 25 |
| 低延迟交易类 | 在 21+ 评估 分代 ZGC;25 可看 分代 Shenandoah 与对象头压缩收益 |
跨版本升级的最小闭环
- 依赖扫描:
jdeps、构建插件的 bytecode 基线检查、第三方组件官方支持矩阵。 - 编译到目标
--release,打开警告,先消灭非法访问与已移除 API。 - 同负载压测:对比 P99、GC 停顿、CPU、RSS;GC 日志格式变了,看板也要改。
- 灰度:先非核心实例,再按错误预算放量。
头号拦路虎:非法反射 + 已移除 API + 过时 GC / 日志参数。
通用在线服务用 G1 通常够用;堆很大且要 稳定低停顿 再上 ZGC(21+ 优先分代 ZGC)。没有绝对更快,只有停顿 / 吞吐 / 内存的取舍(铁三角见 GC 篇)。
7. 和本系列前几篇怎么对上号
| 理论篇知识点 | 版本演进里对应的变化 |
|---|---|
| 方法区 / 对象头 | Metaspace(8);Compact Object Headers(25) |
| G1 / ZGC / 分代 | 9+ G1 默认;21 分代 ZGC;25 分代 Shenandoah |
| JIT 预热 | Leyden / AOT 相关能力持续改善启动 |
| 线程与栈成本 | 21 虚拟线程;25 Scoped Values 正式 |
理论是机制,版本是机制在时间轴上的落点 —— 两者叠在一起,选型与调参才不会凭感觉。
监控与排障可继续看 Grafana JVM 监控 与 Arthas 诊断。
延伸阅读
- Oracle. Java SE Support Roadmap —— LTS 支持周期。
- Oracle. Significant Changes in the JDK —— 官方迁移视角的版本差异。
- OpenJDK. JDK 21 · JDK 25 · JEP Index —— 以 JEP / GA 为准,Preview 勿当稳定契约。