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。默认值才是升级事故的高发区,因为没有人会去读你没写的那个开关。

这张图想让你看见的是: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import java.util.Arrays;

public class TlsGroupProbe {
public static void main(String[] args) throws Exception {
SSLContext ctx = SSLContext.getDefault();
try (SSLSocket socket = (SSLSocket) ctx.getSocketFactory()
.createSocket("example.com", 443)) {

SSLParameters params = socket.getSSLParameters();
System.out.println("default groups = "
+ Arrays.toString(params.getNamedGroups()));

// 只允许混合后量子组与传统 x25519,便于观察协商结果
params.setNamedGroups(new String[] {
"X25519MLKEM768", "x25519"
});
socket.setSSLParameters(params);
socket.startHandshake();

System.out.println("protocol = " + socket.getSession().getProtocol());
System.out.println("cipher = " + socket.getSession().getCipherSuite());
}
}
}

这段代码在说明什么:第一,后量子能力在 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 编码仍然只是第三预览,证书与签名的文本流转还没有完全固化。

这张图想让你看见的是“升级 ≠ 生效”。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 往往更难看。

这张图想让你看见的是 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 更需要提前打招呼。

七、今天就能做的六件事

  1. 装一个 JDK 27,先只做一件事:跑上面那段 TlsGroupProbe,确认默认组列表第一位是不是 X25519MLKEM768。
  2. 在预发环境开一次 JFR,重点看 jdk.VirtualThreadPinned 事件还有没有,以及 reason 字段指向哪里。
  3. 用 openssl s_client 带上混合组,对你们最关键的外部依赖做一次探测,记下哪些对端会回落。
  4. 盘点证书与 TLS 终止点数量,产出一张“我控制不了几跳”的表,这决定 PQC 的排期。
  5. 在 CI 里加一个 25 与 27 双 JDK 的矩阵构建,先不求通过,只求把失败清单攒出来。
  6. 把团队里所有“诊断类”启动参数列一份清单,逐个标注在 27 上是否仍然生效。

最后回到标题。27 不是 LTS,这不影响它值得被认真对待;真正会伤人的不是版本号写错,而是把“默认值变更”当成“没有变更”。后量子密码、G1、紧凑对象头,这三件事的共同点就是:你什么都不做,它们也会生效。