JDK 27 不是 LTS:后量子密码默认生效了,虚拟线程之后 JVM 还想解决什么
JDK 27 不是 LTS:后量子密码默认生效了,虚拟线程之后 JVM 还想解决什么
2026 年 9 月 15 日,JDK 27 达到 GA。随后中文技术圈出现了一批标题几乎一样的文章,核心都写着同一句话:JDK 27 是 LTS。
这句话是错的,而且错得很有代表性,它把“两年一次的大版本”和“LTS”混成了一个词。Oracle 的 Java SE 支持路线图原文只列了五个 LTS:8、11、17、21、25,并明确写了下一个计划中的 LTS 是 2027 年 9 月的 Java 29。所以 27 只是一个功能版本,一年后就会被 28 取代。
但纠正标题不是这篇的目的。真正值得问的是另一件事:如果因为“27 不是 LTS”就跳过它,你会漏掉什么?
我的答案是:27 是 29 的预演带。等到 29 LTS 落地时,后量子优先的 TLS、G1 成为全环境默认、紧凑对象头默认开启,这三件事会一次性同时生效,而它们分别改的是你的协议握手、垃圾回收行为和对象布局。27 让你提前一年半,在真实流量和真实堆里把它们验一遍。
一、先把事实钉住:27 到底改了什么
JDK 27 是 JSR 402 的参考实现,一共 9 个 JEP,其中 4 个是正式特性(Final),4 个仍是预览,1 个还在孵化。
| JEP | 名称 | 状态 | 你实际会碰到的地方 |
|---|---|---|---|
| 527 | TLS 1.3 后量子混合密钥交换 | 正式 | 所有走 javax.net.ssl 的 HTTPS 客户端 |
| 523 | G1 成为所有环境的默认收集器 | 正式 | 小堆、小容器、命令行工具 |
| 534 | 紧凑对象头默认开启 | 正式 | 堆占用、heap dump、基于偏移量的黑科技 |
| 536 | JFR 进程内数据脱敏 | 正式 | 生产环境开 JFR 的合规边界 |
| 538 | 加密对象的 PEM 编码 | 第三预览 | 密钥与证书的文本交换 |
| 531 | 惰性常量 | 第三预览 | 启动期初始化的类 |
| 532 | 基本类型的模式匹配 | 第五预览 | switch 里的 int、long |
| 533 | 结构化并发 | 第七预览 | 虚拟线程的任务编排 |
| 537 | Vector API | 第十二孵化 | 手写 SIMD |
看完这张表你会发现一个规律:27 没有一门新语言特性转正,它的重量全压在“默认值”上。四个正式特性里,有三个是把某个东西改成默认,而不是新增一个 API。默认值才是升级事故的高发区,因为没有人会去读你没写的那个开关。
flowchart LR
R[JDK 27 GA 2026-09-15] --> P[协议层]
R --> T[运行时默认值]
R --> L[语言与 API]
P --> P1[JEP 527 TLS 1.3 后量子混合密钥交换]
P --> P2[JEP 538 加密对象 PEM 编码 第三预览]
T --> T1[JEP 523 G1 全环境默认]
T --> T2[JEP 534 紧凑对象头默认]
T --> T3[JEP 536 JFR 数据脱敏]
L --> L1[JEP 533 结构化并发 第七预览]
L --> L2[JEP 531 惰性常量 第三预览]
L --> L3[JEP 532 基本类型模式 第五预览]
L --> L4[JEP 537 Vector API 第十二孵化]
这张图想让你看见的是:27 的变更不是“多了一个功能”,而是三条独立的传导路径。协议层改的是你和别人握手的方式,运行时默认值改的是你自己的性能曲线,语言与 API 则大多还没定稿。升级评估必须按这三条路径分开做,混在一起讨论就会出现“这个版本好像没什么大变化”的错觉。
二、后量子密码:ML-KEM 和 ML-DSA 不是 27 才有的
这是最容易被误读的一点。很多文章写“JDK 27 引入后量子密码”,准确的说法应该是:JDK 27 让后量子密码第一次默认生效,但算法本身在 JDK 24 就已经进来了。
| 版本 | JEP | 内容 | 边界 |
|---|---|---|---|
| Java 21 | 452 | KEM API | 只提供密钥封装的抽象接口 |
| JDK 24 | 496 | ML-KEM,NIST FIPS 203 | javax.crypto 层的 KeyPairGenerator、KEM |
| JDK 24 | 497 | ML-DSA,NIST FIPS 204,支持 ML-DSA-44/65/87 | 不做 JAR 签名,也不做 TLS |
| JDK 27 | 527 | TLS 1.3 混合密钥交换 | 只做 1.3,只做 javax.net.ssl |
JEP 527 定义了三套混合组:X25519MLKEM768、SecP256r1MLKEM768、SecP384r1MLKEM1024。默认只开第一套,而且把它排在了客户端首选列表的第一位,完整默认顺序是 X25519MLKEM768、x25519、secp256r1、secp384r1、secp521r1、x448,再加三个 ffdhe 组。
注意 JEP 527 的两个非目标:不做纯 ML-KEM 的密钥交换,也不做 TLS 1.3 以外的版本。这说明 JDK 的策略是“先保命,不激进”——混合方案只要有一个算法没被攻破就是安全的,而纯后量子方案还得等更多密码分析。
下面这段代码是我用来确认本机 JDK 到底发了哪些组、协商成了什么。
1 | import javax.net.ssl.SSLContext; |
这段代码在说明什么:第一,后量子能力在 JDK 27 里是“看不见的默认值”,你不改一行代码就会带上 X25519MLKEM768 的 key share,打出来的默认组列表就是证据;第二,setNamedGroups 是显式开关,当你要验证“对端到底支不支持 PQC”时,把组裁到只剩两个,握手失败就说明对端不认识这个组,这比读文档可靠;第三,也是我提醒自己的地方——如果生产代码里有人手工 setNamedGroups 或设置了 jdk.tls.namedGroups,那 JDK 27 的默认值对你完全无效,你升级完会发现“没变化”,其实是被自己的配置挡住了。
三、为什么到 2026 年才认真做:成本在证书和协议层
我的判断是:后量子迁移更像当年 TLS 1.2 升 1.3,而不是 JDK 8 升 11。业务代码几乎不用动,工作量全部堆在证书链、中间设备和客户端矩阵上。
依据就在 JEP 里。JEP 497 在交付时明确写了不做 TLS、不做 JAR 签名,因为标准还没到位;JEP 527 又明确只做 TLS 1.3 的混合组。也就是说,密钥交换这一层现在有了,但“用 ML-DSA 签名的证书”这条链还在补:JDK 27 里 PEM 编码仍然只是第三预览,证书与签名的文本流转还没有完全固化。
sequenceDiagram
participant C as TLS 客户端 JDK 27
participant S as 服务端 支持混合组
participant M as 中间设备 TLS 终止
C->>S: ClientHello 携带 X25519MLKEM768 与 x25519 两个 key share
S->>C: ServerHello 选中 X25519MLKEM768
Note over C,S: 会话密钥同时依赖 ML-KEM-768 与 X25519
C->>M: 同样握手 但中间设备只认识 x25519
M->>C: 回落到 x25519 没有后量子保护
Note over C,M: 观测点 必须记录实际协商组 而不是记录客户端支持组
这张图想让你看见的是“升级 ≠ 生效”。JDK 升级只改变了客户端愿意提供什么,真正决定防护强度的是链路上最弱的那一跳。TLS 终止设备、七层网关、老版本的反向代理,任何一处只认 x25519,握手就会安静地退回传统算法,日志里不会报警。
所以这里有一个反直觉的代价:升级之后你的 ClientHello 变大了。混合组要同时传 ML-KEM 的封装公钥和 X25519 的 key share,单个握手包增加的数据量足以在某些 MTU 敏感或者有分片限制的老链路上出问题。我见过的情况是握手偶发超时,抓包才看出是 ClientHello 被拆包。如果你的业务里还有自建的长连接网关、有些老旧硬件负载均衡,这一点必须先测,不要等灰度。
反过来说,这个观点也有不成立的时候:如果你的服务是纯内部 mTLS、CA 和客户端都在自己手里、链路上没有任何 TLS 终止设备,那 PQC 迁移的成本确实很低,甚至可以一次推完。判断标准不是“我们有没有加密流量”,而是“链路上有几个我控制不了的 TLS 端点”。
四、虚拟线程:pinning 修好了,瓶颈换了个地方
虚拟线程在 Java 21 由 JEP 444 转正,当时最著名的问题就是 synchronized 会钉住载体线程。这个问题在 JDK 24 由 JEP 491 解决:在 synchronized 方法和语句里阻塞的虚拟线程现在会释放载体线程。JEP 491 同时覆盖了另一种情形——虚拟线程进入 monitor 竞争或调用 Object.wait 时,以前也是钉住的。
我踩过的坑正好在这里。JDK 21 时代排查 pinning,大家的肌肉记忆是加 -Djdk.tracePinnedThreads=full 看堆栈。到了 JDK 24 以后,JEP 491 明确说这个系统属性不再需要了,诊断入口变成 JFR 的 jdk.VirtualThreadPinned 事件,而且事件本身被增强了,会带上“为什么钉住”和“载体线程是谁”。我第一次在 JDK 25 上排查一个卡顿,加了这个参数、什么都没打出来,还以为问题消失了,实际上是那个开关已经不管事了。教训很朴素:升级 JVM 时,把诊断参数当作 API 一起做兼容性检查。
那现在还会钉住吗?会。JEP 491 保留了事件用于其他 pinning 场景,最典型的是虚拟线程调用 native 代码(无论是 native 方法还是 Foreign Function & Memory 回调),native 代码再回调进 Java 执行阻塞操作或等 monitor。这类问题靠升级 JVM 解决不了,只能靠替换实现或者把 native 调用挪到平台线程池。
| 场景 | 虚拟线程是否划算 | 原因 |
|---|---|---|
| 下游 HTTP、DB 这类阻塞 IO,并发量大 | 划算 | 线程数上限不再是约束,代码模型不变 |
| 已经在用响应式框架且压得很稳 | 收益有限 | 瓶颈本来就不在线程,多一层调度反而难观测 |
| CPU 密集计算 | 不划算 | 并行度等于核数,虚拟线程只多了调度开销 |
| 大量 synchronized 且竞争激烈 | 视版本 | 21 上会 pin,24 以上不 pin 但仍然串行化 |
| 依赖 ThreadLocal 传递上下文 | 要改 | 线程可能几十万个,ThreadLocal 副本变成内存问题 |
我的理解是:虚拟线程解决的是“线程数量上限”,不是“吞吐上限”。一旦你把线程成本降到接近零,瓶颈会立刻平移到下游那几个有固定上限的资源上——数据库连接池、HTTP 客户端连接池、限流信号量、CPU 核数。给一个 20 个连接的池配上 10 万个虚拟线程,排队的位置只是从线程池搬到了连接池,而超时语义变了,p99 往往更难看。
stateDiagram-v2
[*] --> 新建
新建 --> 运行中 : 启动
运行中 --> 阻塞 : 发起阻塞式调用
阻塞 --> 已卸载 : 让出载体线程
已卸载 --> 运行中 : 数据就绪 重新挂载
运行中 --> 钉住 : 进入 native 或 FFM 回调
钉住 --> 运行中 : native 返回
运行中 --> [*] : 任务结束
这张图想让你看见的是 pinning 的位置变了。以前“阻塞就钉住”,现在只有“进入 native 边界”才会钉住,而且它是一条更隐蔽的路径:JFR 里出现 jdk.VirtualThreadPinned 时,你该去看的是 native 调用栈,而不是去搜代码里的 synchronized 关键字。
五、Loom 之后:结构化并发为什么还在第七次预览
JDK 27 里结构化并发(JEP 533)是第七次预览。对比一下:虚拟线程 21 转正,Scoped Values 在 25 由 JEP 506 转正,而结构化并发还在预览。这个时间差很说明问题。
我的理解是:卡住的不是运行时能力,而是 API 形状和错误语义。StructuredTaskScope 要同时回答好几个没有标准答案的问题:作用域关闭时是取消还是等待、异常怎么聚合、超时归谁管、和现有的 ExecutorService、Spring 的事务上下文、MDC 怎么共存。这些问题的每一个答案都会反过来影响 API 长什么样,所以它反复预览是可以理解的。
这里我不太同意一种流行说法:“用了虚拟线程,代码自然就更结构化。”这两件事是分开的。虚拟线程给的是调度能力,结构化并发给的是生命周期约束。没有后者,你写的还是 executor.submit 加一堆 Future.get,只是这些 Future 跑在更便宜的线程上,错误处理和取消传播该丢还是丢。而且因为它在第七次预览,你在生产里用它,就要接受每半年跟着预览 API 改一次代码的代价。我的建议是:框架和中间件作者现在就该跟,业务代码先用 Java 21 就稳定存在的结构化替代方案(作用域内的 try-with-resources 加显式取消)把边界画出来,等它转正再换。
六、我的判断:现在该不该升
先给结论,再给条件。
结论:2026 年下半年的生产默认版本仍然应该是 25 LTS;27 适合作为“29 LTS 的预演环境”,在非核心链路和压测环境里先跑起来。理由是 29 会把 G1 默认、紧凑对象头默认、PQC 优先这三件事一起带进 LTS,那时你没有“只改一个变量”的机会,而 27 给了你这个机会。
这个判断在什么情况下不成立:如果你的发布节奏是年更甚至更慢,团队没有余力维护第二套 JDK,那 27 对你没有收益,老老实实等 29。预演带的价值建立在“你确实会去做验证”这个前提上,不做验证的预演只是提前引入风险。
升级前必须验证的东西:
| 验证项 | 怎么验 | 为什么是它 |
|---|---|---|
| 字节码与依赖 | 用 27 的 javac 编译、用 27 跑 25 的产物,双向各跑一遍测试 | 27 没有移除类的大动作,但依赖里的字节码生成器常跟不上 |
| 加密库与 TLS 观测 | 打印默认组列表,抓包确认协商组,压测 ClientHello 体积 | 后量子默认开启是“静默生效”的典型 |
| GraalVM / native image | 确认所用发行版是否已支持 27 | 该项本文未能核实,标记为待核 |
| GC 行为 | 小堆、小容器场景重新量启动时间与内存占用 | JEP 523 让原本选 Serial 的场景改用 G1 |
| 对象布局假设 | 重跑 JOL 或依赖 Unsafe 偏移量的代码,检查 heap dump 工具 | JEP 534 让紧凑对象头默认开启,字段偏移会变 |
| 诊断参数 | 逐个检查 -XX 与系统属性是否还有效 | 前面 jdk.tracePinnedThreads 的坑就是例子 |
关于紧凑对象头,我多说一句:它的收益是实打实的堆占用下降,但代价是“你过去的性能观测口径全部变了”。用 JOL 量出来的对象大小、基于字段偏移写的序列化、把对象头当 12 字节来算的容量规划,全都要重算。这类变化不会让测试失败,只会让你的监控图表悄悄变形——我认为这是 27 最容易被忽略的部分,比 PQC 更需要提前打招呼。
七、今天就能做的六件事
- 装一个 JDK 27,先只做一件事:跑上面那段 TlsGroupProbe,确认默认组列表第一位是不是 X25519MLKEM768。
- 在预发环境开一次 JFR,重点看 jdk.VirtualThreadPinned 事件还有没有,以及 reason 字段指向哪里。
- 用 openssl s_client 带上混合组,对你们最关键的外部依赖做一次探测,记下哪些对端会回落。
- 盘点证书与 TLS 终止点数量,产出一张“我控制不了几跳”的表,这决定 PQC 的排期。
- 在 CI 里加一个 25 与 27 双 JDK 的矩阵构建,先不求通过,只求把失败清单攒出来。
- 把团队里所有“诊断类”启动参数列一份清单,逐个标注在 27 上是否仍然生效。
最后回到标题。27 不是 LTS,这不影响它值得被认真对待;真正会伤人的不是版本号写错,而是把“默认值变更”当成“没有变更”。后量子密码、G1、紧凑对象头,这三件事的共同点就是:你什么都不做,它们也会生效。





