Skip to content
Charles Shao
Go back

JVM 深入(四):JDK 核心版本演进 —— 从 8 到 25 的 LTS 变迁

–views

JVM 系列的理论三篇已经把内存与类加载、垃圾回收以及JIT 与调优的底层机制讲透了。但落到工程实践上,还有一个绕不开的核心问题:线上到底该钉在哪个 JDK 版本?从 8 升到 17、21 乃至 25,除了语言特性的更迭,运行时机制和升级成本究竟变了什么?

本文是 JVM 系列的第 4 篇(共 6 篇)。 全系列:

  1. 入门篇 · 内存架构与类加载
  2. 核心篇 · 垃圾回收全解
  3. 进阶篇 · JIT、内存模型与调优
  4. JDK 版本差异精讲(8 → 25)(本篇)
  5. Grafana JVM 监控分析
  6. Arthas 诊断

一句话定位:前三篇的底层机制最终都钉在某一版 JDK 的默认值上,本篇以 LTS 为主线串联从 8 到 25 的运行时变迁、废弃禁区与生产选型路径。

本篇不以长篇累牍的 JEP 清单铺开,而是以 LTS(Long-Term Support) 为主线,把真正影响代码写法、默认行为与业务迁移成本的关键差异讲清楚,并给出可落地的生产选型建议。

TL;DR

Table of contents

Open Table of contents

1. 先搞清:什么是 LTS

JDK 9 之后,Oracle 与 OpenJDK 社区采用了每 6 个月发一版的节奏。在这些版本中,只有少数被标记为 LTS(长期支持版):厂商会为其提供更长周期的安全更新与漏洞补丁,且整个技术生态(包括 Spring、各类中间件、云原生镜像、监控 Agent)也会优先去对齐这些版本。

角色版本含义
LTS8、11、17、21、25…生产基线;支持周期以年计
非 LTS(Feature Release)9–10、12–16、18–20、22–24…功能试验田;通常只维护到下一版发布

一句话本质:非 LTS 是功能快递与短期试验田,而 LTS 是能够长期承载生产流量的稳固基站。当团队缺乏专人持续跟进版本演进时,钉 LTS 几乎总是更优解。

JDK LTS 时间线:8(2014)→ 11(2018)→ 17(2021)→ 21(2023)→ 25(2025) 自 21 起 LTS 约两年一档;图中箭头标注相邻 LTS 的大致间隔。

各 LTS 代表性能力一览:语言/API(绿)、运行时(蓝)、破坏性变更(粉) 绿 = 语言与标准库;蓝 = 运行时 / 性能;粉 = 移除或强约束 —— 升级时粉条最容易踩坑。

关键变化哪版一句话
PermGen → Metaspace8类元数据出堆并使用本地内存,有效缓解 PermGen space OOM
默认 GC9+Parallel → G1;CMS 在 14 被彻底删除
模块系统 JPMS9 / 11 LTSJDK 自身实现模块化;应用层可不写 module-info,但内部 API 渐难反射
Lambda / Stream8行为参数化,配合声明式集合数据流水线
Records / Sealed / Pattern Matching16–17引入透明数据载体、限制继承白名单以及 instanceof 模式绑定
虚拟线程21由 JVM 调度的轻量线程,成为解决 I/O 密集型高并发的现代答案
Scoped Values25 正式虚拟线程时代替代部分 ThreadLocal 职能的作用域共享机制

2. 运行时关键差异:Metaspace、G1、日志

语言特性看得见、摸得着,但默认 GC 的更替、方法区底层实现的切换、以及日志开关规则的变化,却经常在服务升级后才以线上事故的形式暴露出来。

运行时默认值变迁:永久代→元空间、Parallel→G1、PrintGC→Xlog、ZGC→分代 ZGC/Shenandoah

2.1 PermGen → Metaspace(JDK 8)

永久代(≤7)元空间(8+)
存什么类元信息、方法代码、常量池等同类信息(字符串常量池 JDK 7 已先挪进堆)
内存位置JVM 堆内一块固定且极难扩容的区域本地内存(Native),默认可按需弹性伸缩
典型故障java.lang.OutOfMemoryError: PermGen spaceMetaspace(常因反射、CGLIB 或热部署导致动态类过多)
调参-XX:PermSize / MaxPermSize-XX:MetaspaceSize / MaxMetaspaceSize

演进逻辑:永久代的大小在启动时极难准确预估,一旦加载的类过多就会直接触发 OOM。为了解决这个痛点,JDK 8 引入了使用本地内存的元空间,默认具备更强的弹性扩容能力。需要强调的是,这并不意味着「永远不会 OOM」——如果框架动态生成类的行为失控,照样会把 Metaspace 打满。正因如此,虽然对象实例依然在堆上分配,但类元数据已经被整体移到了元空间,这正是入门篇中方法区实现变迁的核心所在。

2.2 默认 GC:Parallel → G1,CMS 退场

2.3 GC 日志:-XX:+PrintGCDetails → -Xlog:gc*

随着统一日志框架的引入,曾经熟悉的旧旗标(如 -XX:+PrintGCDetails、-XX:+PrintGCDateStamps)在较新版本中逐步失效或被替代。新的配置规范变更为 -Xlog:gc*(可按需叠加 gc+heap、gc+age 等标签)。需要注意的是,统一日志并非只针对 GC,它是整套 JVM 状态暴露的基础。


3. 语言与 API:从 8 到 17

3.1 JDK 8:Lambda、Stream、接口默认方法

顺带一提集合类的底层实现演进:自 JDK 8 起,当 HashMap 中的哈希冲突导致链表过长(默认树化阈值 8,且容量 ≥ 64)时,结构将转为红黑树,从而把最坏查找时间复杂度从 O(n) 降至 O(log n);同时扩容时的插入方式由头插改为尾插,有效降低了并发场景下形成死循环的风险(尽管 HashMap 本身仍非线程安全,并发场景下必须使用 ConcurrentHashMap)。

3.2 JDK 11:var、HttpClient、模块化落地

模块系统(JPMS,9 引入,11 作为 LTS 广泛落地)

JDK 11 最深远的影响在于模块化。首先,JDK 自身完成了模块化拆分,强化了内部代码的封装,有效遏制了过去「随意反射进入 sun.* 包」的乱象。对于应用层而言,虽然可以继续沿用传统的 classpath(即不写 module-info),但在升级后会越来越频繁地遇到非法反射访问的警告甚至报错。

在具体实践中,核心关键词包括 module-info.java、requires / exports,以及模块路径与类路径的严格区分。这也带来了最典型的升级痛点:诸如 JAXB 等 Java EE 与 CORBA 相关模块已从 JDK 中彻底移除,若业务仍需使用必须显式引入外部依赖;同时,像 sun.misc.BASE64Encoder 这样的内部 API 也不再允许直接依赖。

3.3 JDK 17:Records、Sealed、Pattern Matching、Text Blocks

JDK 17 将 12 至 16 预览期间沉淀的一批高质量语言特性固化为正式能力,它也是 Spring Boot 3 等现代生态对齐的基础版本(要求 JDK 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 {}

在运行时侧,G1 的默认地位此时已十分稳固。与此同时,强封装 JDK 内部 API 的策略默认拒绝了大量非法反射。过去那些依靠 --add-opens 参数勉强运行的陈旧依赖库,到了 JDK 17 往往会彻底暴露兼容性问题,这正是许多老项目升级时遇到的首要阻力。


4. 并发分水岭:虚拟线程(JDK 21)

JDK 21 是近年来对服务端架构影响最深远的 LTS —— 核心关键词是 Virtual Threads(虚拟线程,Project Loom)。

平台线程(Platform Thread)虚拟线程(Virtual Thread)
调度操作系统(OS)调度JVM 调度
成本栈通常占用 MB 级,几千到上万就吃紧极轻量,可支撑百万级并发任务
模型依赖线程池进行精心限流与复用使得「一个请求一个线程」的模型重新成立
擅长CPU 密集型任务仍需靠它扛住阻塞 I/O 密集型(吞吐型并发)

机制三词

  1. Carrier(载体线程):海量的虚拟线程实际上是挂载在少量的平台线程之上运行的。
  2. Mount / Unmount:当虚拟线程遇到阻塞 I/O 时,它可以从载体线程上卸载(unmount),让出载体线程去执行其他就绪的虚拟线程;待 I/O 完成后,再重新挂载(mount)继续执行。
  3. Pinning(钉住):如果在虚拟线程内部持有了 synchronized 监视器,或者进入了某些 native 栈帧,虚拟线程可能无法被卸载,从而将载体线程死死钉住(pinning),导致系统吞吐量回落。在 JDK 21 至 23 版本上,应对策略是优先使用 ReentrantLock 等 JUC 锁,并避免在虚拟线程中堆积繁重的 CPU 计算或大内存的线程局部状态;同时可通过添加 -Djdk.tracePinnedThreads 参数或 JFR 的 VirtualThreadPinned 事件来监控此类现象。需要强调的是,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 同期值得记住的演进


5. JDK 25:当前最新 LTS

JDK 25 于 2025-09-16 正式 GA,接力 JDK 21 成为最新的 LTS 版本。相对于 21,它更加聚焦于 运行时效率提升、AOT 预热易用性、可观测性增强,同时完成了若干长期酝酿 API 的正式收官。

能力含义
Scoped Values(正式)提供作用域内的只读共享机制,在虚拟线程海量并发场景下,比滥用无上限的 ThreadLocal 更安全且语义更清晰。
Compact Object Headers压缩对象头,直接降低堆内存占用 —— 这正呼应了入门篇中探讨的对象头结构知识。
分代 Shenandoah与分代 ZGC 并列,为低延迟 GC 阵营提供了又一强有力的可选项。
AOT / Leyden 易用性大幅改善应用的冷启动速度与预热表现(这也对照了进阶篇中详述的 JIT 预热痛点)。
Preview / Incubator 特性Structured Concurrency、Primitive Patterns、Vector API 等尚未稳定,核心业务路径上切勿盲目押注。

定位:新项目首推 JDK 21(这也是目前生态最为成熟的「现代 Java」基线);如果项目已经在 21 上运行良好且三方依赖均已支持,再去进一步评估 JDK 25 的升级收益。永远不要把 Preview 阶段的特性当作稳定契约写进核心代码中。


6. 升级路径与选型

生产选型路径:离开 8 → 11/17 存量 → 21 新项目首选 → 可选 25 稳字当头:先离开失控的旧基线,再按生态成熟度跃进。

现状建议
仍在 8尽快规划迁出;根据 Spring 等底层框架的约束,优先将目标定为 17 或 21。
停在 11规划向 17/21 升级;重点排查和处理模块封装限制与 Java EE 依赖缺失。
已在 17 且稳定可暂时防守;新服务直接采用 21,存量系统则择机跳跃升级。
新项目默认定在 21;如果团队有意愿跟进最新 LTS 且依赖库支持良好,则评估 25。
低延迟交易类在 JDK 21+ 环境下重点评估分代 ZGC;如果上了 25,可对比考察分代 Shenandoah 与对象头压缩带来的额外收益。

跨版本升级的最小闭环

  1. 依赖扫描:使用 jdeps、构建插件的 bytecode 基线检查工具,并确认第三方组件的官方支持矩阵。
  2. 编译到目标 --release:开启全部警告,首先消灭所有的非法访问与已移除 API。
  3. 同负载压测:在测试环境中严格对比 P99 延迟、GC 停顿时间、CPU 利用率以及 RSS(常驻内存)。由于新版 GC 日志格式变了,监控看板的抓取规则也要同步修改。
  4. 灰度发布:先在非核心实例上试刀,观察无异常后再按错误预算逐步放量。

升级路上的头号拦路虎依然是:非法反射调用 + 依赖了已移除的 API + 启动脚本里写死了过时的 GC 或日志参数。

一般而言,通用在线服务使用 G1 通常就够用了;只有当应用配置了极大的堆内存且对稳定低停顿有严苛要求时,才需要考虑换上 ZGC(在 JDK 21+ 优先选用分代 ZGC)。正如在核心篇中所强调的,垃圾回收从来没有绝对的更快,只有在停顿、吞吐量和内存占用这「铁三角」之间的权衡与取舍。


7. 和本系列前几篇怎么对上号

理论篇知识点版本演进里对应的变化
方法区 / 对象头Metaspace(JDK 8);Compact Object Headers(JDK 25)
G1 / ZGC / 分代9+ G1 成为默认;21 引入分代 ZGC;25 引入分代 Shenandoah
JIT 预热Project Leyden 与 AOT 相关能力在持续改善应用的冷启动与预热阶段
线程与栈成本21 虚拟线程登场;25 Scoped Values 正式确立

和前几篇对上号:理论三篇讲透的是底层机制,而版本演进则是这些机制在时间轴上的具体落点。只有把这两者叠加在一起,线上的选型与调参才不会沦为拍脑袋的「玄学」。

当底层机制与版本基线都梳理清楚后,要想真正在线上扛住高并发流量,就必须把 JVM 的运行状态拉出来展示在看板上。接下来,请继续阅读 Grafana JVM 监控分析 与 Arthas 诊断。

延伸阅读


–views
Share this post on:

Previous Post
Grafana JVM 监控分析:从指标采集到看板解读的实战指南
Next Post
JVM 深入(三):JIT 编译与内存模型 —— 从底层原理到调优实战