Java 25 LTS 与虚拟线程这三年:从 pinning 之痛到放心用

2025 年 9 月 16 日,JDK 25 正式发布,这是继 21 之后的又一个 LTS。它把过去两年预览的几件大事收了口:紧凑对象头从实验特性转正(JEP 519),简单源文件与实例主方法定稿(JEP 512),Scoped Values 定稿(JEP 506);AOT 缓存也借助 Project Leyden 在 24 打底、25 铺路,走到「一条命令」的可用形态。同一个节点还有另一层意义:虚拟线程自 Java 21(2023 年 9 月)落地已满三年,synchronized pinning 在 JDK 24 被 JEP 491 修复,主流框架全面接上。本文先按官方 JEP 过一遍 25 的关键特性,再用一半篇幅复盘虚拟线程这三年的生产经验与取舍。

Java 25 全景:一次 LTS 该有的样子

JDK 25 共包含 18 个 JEP,与后端日常关系最大的如下:

JEP 内容 状态
519 紧凑对象头 正式特性
512 简单源文件与实例主方法 正式特性
511 模块导入声明 正式特性
513 灵活构造器体 正式特性
506 Scoped Values 正式特性
510 密钥派生函数 API 正式特性
514 / 515 AOT 命令行优化 / AOT 方法画像 正式特性
521 分代 Shenandoah 正式特性
518 / 520 JFR 协作式采样 / 方法计时与追踪 正式特性
505 结构化并发 第五次预览
507 模式匹配中的原始类型 第三次预览
502 Stable Values 预览
508 Vector API 第十次孵化

可观测性这条线值得单独点一句:JFR 拿到协作式采样(518)与方法计时追踪(520),CPU 时间剖析以实验特性入场(509),排障工具箱明显变厚。移除类变化只有一件:32 位 x86 端口被移除(503),还在维护老硬件的团队要留意。下面挑四组对生产影响最大的展开。

紧凑对象头:每个对象省一点,堆上省一大截

64 位 JVM 上每个对象都背着一段对象头:mark word 加类指针,在 JEP 450 的原话里占 96 位(12 字节)到 128 位(16 字节),取决于压缩指针配置。JEP 519(前身正是 JDK 24 的实验特性 JEP 450)把头部压进单个 64 位字——类指针收窄塞进低位、identity hash 等信息重新编排,并为 Project Valhalla 预留 4 个位,未来还能用 Project Lilliput 的技术继续压。

开启方式是一个正式开关 -XX:+UseCompactObjectHeaders,25 起不再需要 UnlockExperimentalVMOptions。默认尚未打开——把紧凑布局变成默认是后续 JEP(534)的目标,截至 2026-10-10 仍是待办。

官方 JEP 页面给出的验证数据:SPECjbb2015 在一组配置下堆占用少 22%、CPU 时间少 8%;另一组配置里 G1 与 Parallel 收集器的 GC 次数都少 15%;一个并行 JSON 解析基准快了 10%。收益本质是小对象堆的内存密度与缓存局部性——对象个数越多、单个对象越小的服务越明显。可信度方面,Oracle 跑了全量测试套件,Amazon 在数百个生产服务上验证过(其中不少用 backport 在 JDK 21 和 17 上先行跑了很久)。

实操建议:多数服务可以直接灰度这个 flag;评估收益看堆里的对象个数而不是堆大小——大量字符串、包装类型、领域小对象的业务服务收益最大,而长命大对象为主的缓存服务收益有限。

小结:这是近年少见的「一行配置、全线受益」的 JVM 改进,25 升级评估里应优先试它。

语法收口:入门第一课变了

JEP 512 定稿后,Java 的 hello world 从「public class 加 public static void main 加 String 数组」变成:

void main() {
    IO.println("Hello, World!");
}

没有 class 声明、没有静态修饰、没有 import——紧凑源文件隐式导入整个 java.base 模块(与定稿的 JEP 511 模块导入声明同一机制),新 IO 类直接放进 java.lang。它刻意避开两个坑:不是新方言(同一套 javac 与工具链),也方便长大——包一层 class、补上 import,main 一行不用改。启动器对 main 的选择也放宽:优先 main(String[]),其次无参 main(),且允许是实例方法(类需有非私有无参构造器,由启动器实例化后调用)。对教学、脚本工具与原型是实打实的减负。

JEP 513 灵活构造器体则允许在 super(...) 之前执行语句(不得访问 this 实例),参数校验、字段规整可以写进构造器最前面,不必再包一层静态工厂。密码学侧 JEP 510 定义了通用的密钥派生函数 API。这些特性不改变既有代码语义,升级即得。

并发三件套:Scoped Values 定稿,结构化并发第五预览

与虚拟线程配套的两个 API,在 25 走向了不同终点。

Scoped Values(JEP 506)定稿。 ThreadLocal 的三个老毛病——无约束可变、生命周期不可控(池化线程上值跨任务泄漏)、百万线程下继承开销大——被「一次性、不可变、作用域绑定 run 调用」的 ScopedValue 解决:

private static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();

ScopedValue.where(CURRENT_USER, "alice").run(() -> handleRequest());
// run 返回即解绑;子线程经 StructuredTaskScope fork 时零拷贝继承绑定

读取速度接近局部变量,不用手动 remove,也没有池化串值事故。双向传值与可变对象缓存(比如池化的 SimpleDateFormat)仍是 ThreadLocal 的合理场景,其余共享上下文建议逐步迁移。

结构化并发(JEP 505)第五次预览。 作用域改用静态工厂 open() 创建(不再暴露公共构造器),策略交给 Joiner:等全部成功、抢第一个成功、全部等到或自定义谓词提前取消:

try (var scope = StructuredTaskScope.open()) {      // 默认策略:全成功才返回
    var user   = scope.fork(() -> fetchUser(id));
    var orders = scope.fork(() -> fetchOrders(id));
    scope.join();
    return new Profile(user.get(), orders.get());   // join 之后才能 get
}

错误短路、超时级联取消、JSON 线程转储里能看到作用域树,这些都齐了;但它仍要求 --enable-preview——这套 API 从 21 到 25 已经五次改形,OpenJDK 明确要等反馈收敛再定稿(第六次预览已在路上)。生产判断:结构化并发的思想(任务成树、统一取消、错误短路)现在就可以用设计纪律落实,API 本身建议等定稿再上关键路径。

AOT:一条命令,启动更快

Project Leyden 在 JDK 24 交付了 AOT 类加载与链接缓存(JEP 483),25 的 JEP 514 把「训练 + 建缓存」两步合成一条命令:

java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App   # 训练并生成缓存
java -XX:AOTCache=app.aot -cp app.jar com.example.App         # 带缓存启动,更快

JEP 515 再进一步:训练期顺带采集方法画像存进缓存,JIT 启动即有画像、立刻产出优化代码。官方示例里一个 Stream 版 hello world 从 90ms 降到 73ms(快 19%),缓存只增大约 250KB。两点边界要清楚:这是启动与预热优化,不改变稳态吞吐;建缓存进程的内存需求约为应用堆配置的两倍(也可以在小机器上训练、大机器上建缓存,两步可拆)。对 Serverless 冷启动、发布后的首波流量、批处理任务的启动段,值得投入一个训练流水线。

虚拟线程三年:pinning 的来龙去脉

2023 年 9 月 19 日 Java 21(JEP 444)把虚拟线程转正,当时的生产建议很一致:I/O 密集、高并发、线程每请求的服务值得上;但有一条刺眼的脚注——synchronized 块内阻塞会 pin 住载体线程。机制是这样的:JVM 此前按平台线程记录锁持有者,虚拟线程若在持锁时卸载、另一个虚拟线程挂上同一载体,就会造成「伪持锁」,破坏互斥;于是 JVM 只能不让它卸载。阻塞期间载体(真实的 OS 线程)被白白占住,pinning 一多,调度器并行度(默认 CPU 核数,兜底上限 maxPoolSize 默认 256)被耗尽,轻则吞吐骤降、重则死锁。「把热点 synchronized 换成 ReentrantLock」正是那两年的标准动作,JFR 的 jdk.VirtualThreadPinned 事件(默认 20ms 阈值)是最常用的取证工具。

这不是纸面风险。Netflix 在《Java 21 Virtual Threads – Dude, Where's My Lock?》(2024 年 7 月)里公开了真实事故:Java 17 升 21 后微服务间歇性超时、实例假死,最终定位到部分虚拟线程在 synchronized 路径里被 pin 住等锁,载体线程被逐渐占满、调度失去容量,团队以改用 ReentrantLock 等方式先行规避。更麻烦的是 pin 不一定出在你自己的代码里——链路追踪、日志等依赖库的 synchronized 路径同样会中招,这也解释了为什么「审计调用链上的每一处阻塞点」成为那两年的标准动作。

YDB 团队的死锁复盘是依赖库踩雷的完整标本:他们把 TPC-C 压测终端切到虚拟线程后整个应用冻结——连接池 c3p0 在 synchronized 块里用 Object.wait 等连接,等连接的虚拟线程全部 pin 住载体,持有连接的虚拟线程反而卸载了载体去做 I/O;等方占满全部载体、持方永远跑不完,教科书式死锁。定位靠 jcmd <pid> Thread.dump_to_file(能同时看到载体线程与虚拟线程的挂载关系),修复则是在进入连接池之前先用 Semaphore 限流,让阻塞发生在会卸载的点上——恰好验证了「连接池要独立限流」这条红线。

flowchart TB
    A1["JDK 21:synchronized 与 Object.wait 阻塞时 pin 住载体线程"] --> A2["载体被占满,并发骤降,兜底上限默认 256"]
    A2 --> B1["JDK 24(JEP 491):JVM 按虚拟线程记录锁持有"]
    B1 --> B2["阻塞即卸载,载体可服务其他虚拟线程"]
    B2 --> C1["JDK 25:仅剩 native 帧、类初始化等少数场景会 pin"]

JDK 24 的 JEP 491 重写了监视器实现:锁持有按虚拟线程记录,synchronized 阻塞与 Object.wait() 都会卸载虚拟线程、恢复后可换载体继续,官方称消除了几乎所有 pinning 场景。诊断工具同步演进:jdk.VirtualThreadPinned 事件保留并新增 pin 原因与载体信息;-Djdk.tracePinnedThreads 系统属性因 synchronized 场景消失而被移除(设了也没效果)。仍然会 pin 的只剩少数场景:native 帧里回调 Java 并阻塞、符号解析与类加载、类初始化器。

对生产的含义:还留在 17/21 的服务,升级本身就是最大的 pinning 修复;当年迁到 ReentrantLock 的代码不必改回去——官方也认可两种锁各有所长,锁内不做阻塞 I/O 的老纪律依旧成立。

虚拟线程三年:适用边界与响应式取舍

它不是更快的线程。 JEP 444 的定位三年未变:虚拟线程提高并发度,不提高单线程速度。适合的场景:线程每请求模型下的 I/O 等待(HTTP 客户端、JDBC、消息消费),任务量上千、CPU 占用低。不适合的:CPU 密集计算(并行流与常规线程池仍是正解)与对调度延迟敏感的精细并发。三条用法红线:不要池化虚拟线程(要限并发用 Semaphore);慎用 ThreadLocal 做重缓存(百万线程下内存放大,迁 ScopedValue);数据库连接是真实资源,连接池大小要独立评估、独立限流,虚拟线程不会把连接变多。

生态配套三年内基本到位:Spring Boot 3.2 起 spring.threads.virtual.enabled 一个开关把 Tomcat/Jetty 请求处理、@Async、Kafka 与 RabbitMQ 监听切到虚拟线程(基于 Spring Boot 官方发布说明);注意开启后线程池相关配置会被忽略,限流要另接。

与响应式编程的取舍,今天的答案比三年前清晰得多。当年选 WebFlux/Reactor/Vert.x 的核心理由只有一个:阻塞式线程模型撑不住 I/O 并发。虚拟线程拿走了这条理由——阻塞写法、非阻塞的伸缩性,可读性与堆栈调试都回到普通线程。观点性结论,供参考:新项目按「虚拟线程优先」评估,响应式收窄为流式背压管线与函数式编排的专项工具;已在 WebFlux 上稳定运行、团队有响应式储备的,迁移存量的收益有限、回归风险不小,混合形态(Servlet 栈加虚拟线程,或内部用虚拟线程包阻塞调用)反而是常态。

生产建议清单

  • 升级路径:至少 21;有 pinning 痛点直接上 24/25(JEP 491)。截至 2026-10-10,25 是最新 LTS,两年的支持窗口对得起迁移成本。
  • 观测先行:开 JFR 录制 jdk.VirtualThreadPinned,压测暴露 pin 点与耗时分布,再开框架的虚拟线程开关。
  • 配套限流:Semaphore 控制对下游的并发,连接池大小独立评估,别让「线程无限」放大成「连接打满」。
  • 代码习惯:锁内不做阻塞 I/O 的纪律不变;synchronized 不必再回避;ThreadLocal 重的路径排期迁 Scoped Values。
  • 新 API 节奏:Scoped Values 已定稿可迁;结构化并发等定稿,先用它的思想改任务组织。

一句话总结:虚拟线程三年走完了「能用、好用、放心用」的全程,Java 25 则把内存、启动、入门与观测四面收口——这轮 LTS 的主题是确定性,值得认真升。

参考资料

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?