Administrator
发布于 2025-07-20 / 6649 阅读
145

JDK 24 新特性与面向 AI 时代的演进

为什么我们决定在非 LTS 版本上跑生产

JDK 24 今年三月发布,是非 LTS 版本。我们公司原来的规矩是只用 LTS,所以 JDK 24 刚出来时没人提议升级。

改变想法是因为三个具体需求:AI 服务里的虚拟线程 pinning 问题、推理结果回写服务的大堆小对象内存压力、以及函数计算场景的冷启动。这三件事恰好分别对应 JDK 24 里的三个 JEP。我们选了四个服务做试点,跑了四个月,这篇是完整的实测数据和升级过程。

先说结论:我们升级了什么

不是所有服务都升。目前的状态:

服务JDK升级理由
AI 网关21 → 24JEP 491 解决虚拟线程 pinning
推理结果回写21 → 24JEP 404 分代 Shenandoah + JEP 450 紧凑对象头
文档解析入口21 → 24JEP 483 AOT 缓存降冷启动
核心交易链路21(不动)非 LTS 不进核心链路

核心交易链路不动,这是底线。非 LTS 版本只有 6 个月的免费支持窗口,我们评估不了过期后的风险,所以只在"挂了可降级、可快速回退"的服务上用。

JEP 491:虚拟线程不再被 synchronized 卡住

这是对我们最有价值的一个改动,也是我推动升级的主要原因。

背景是这样的:JDK 21 的虚拟线程有个著名的限制——synchronized 块或方法里执行阻塞操作时,虚拟线程无法从载体线程上卸载。因为 JVM 的对象监视器实现依赖载体线程的身份,卸载会导致监视器归属错乱。这时虚拟线程会"钉住"(pin)载体线程,虚拟线程带来的并发优势就没了。

这个问题在 AI 服务里特别要命,因为我们重度依赖各种第三方 SDK(向量库客户端、HTTP 客户端、对象存储 SDK),这些库内部到处是 synchronized。我们排查时用 JFR 抓过:

$ jcmd 1 JFR.start duration=60s settings=profile filename=pin.jfr
$ jfr summary pin.jfr | grep -i virtualthreadpinned
 jdk.VirtualThreadPinned    Event Count: 47,213

6 万次请求里有 4.7 万次发生了 pinning。查看具体位置,绝大部分来自 Redis 客户端和 MongoDB 驱动的 synchronized 方法。

JDK 21/22/23 时代的规避办法是把 synchronized 换成 ReentrantLock。但第三方库里的 synchronized 你改不了。

JEP 491 重写了对象监视器的实现,让监视器的归属跟着虚拟线程而不是载体线程走,从根上解决了 pinning。升级到 JDK 24 之后:

指标JDK 21JDK 24
pinning 事件数(6 万请求)47,2130
5000 并发时 P991840ms412ms
载体线程峰值占用16(打满)7
5000 并发时 CPU5.8 核3.1 核

P99 从 1840ms 降到 412ms,这个提升是纯粹的——我们一行代码都没改,只是换了 JDK 版本。升级之后我把之前为了规避 pinning 改成的 ReentrantLock 全改回了 synchronized,代码更自然。

顺便说一句,如果你还在 JDK 21 上用虚拟线程,强烈建议抓一次 jdk.VirtualThreadPinned 事件看看。这个坑很安静,不会报错,只是让你的性能达不到预期。

JEP 404:分代 Shenandoah 转正成实验特性

我们的推理结果回写服务有个特点:大量短命的小对象。每条 Kafka 消息会创建几十个 DTO、JSON 节点、临时集合,处理完立刻丢弃。堆内存 8GB,Young GC 每分钟 40 多次。

原来用的是 G1,STW 时间在 25~60ms 之间,对下游消费有轻微影响。换成 Shenandoah 的低暂停特性试了试,但老版本的 Shenandoah 是非分代的,对短命对象的回收效率低,反而更慢。

JEP 404 在 JDK 24 里把分代模式加进了 Shenandoah(实验特性),开启方式:

java -XX:+UnlockExperimentalVMOptions \
     -XX:+UseShenandoahGC \
     -XX:ShenandoahGCMode=generational \
     -Xms8g -Xmx8g \
     -jar infer-writer.jar

实测对比(同样的生产流量回放,30 分钟):

GC平均 STW最大 STWGC 总耗时占比吞吐
G1(默认)38ms127ms4.1%基准
Shenandoah(非分代,JDK 21)4ms11ms9.7%-8%
Shenandoah(分代,JDK 24)3ms9ms3.6%+2%

分代模式解决了 Shenandoah 的老问题:既保住了低暂停(最大 STW 从 127ms 降到 9ms),又不牺牲吞吐。这个组合以前在 HotSpot 上是没有的——要低暂停就得接受吞吐损失。

需要说明的是它还是实验特性,我们观察了四个月没有出现问题,但官方不建议在关键业务上用。我们的判断是:这个服务挂了影响的是推理结果的回写时效(延迟几分钟可接受),不是资金或交易,可以承受。

顺带一提 JEP 450:紧凑对象头

同一个服务上我们还开了紧凑对象头(也是实验特性):

java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders ...

它把对象头从 128 位(16 字节)压缩到 64 位(8 字节)。对于大量小对象的场景收益明显——我们那个服务堆内存占用从 5.2GB 降到 4.1GB(降低 21%),Young GC 频率也降了。开启没有任何代码改动,算是白捡的。

JEP 483:提前类加载与链接

这个我在 Leyden 那篇里详细写过,这里只给数据。文档解析入口服务开启 AOT 缓存后:

# 三步流程
$ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -jar app.jar
$ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot
$ java -XX:AOTCache=app.aot -jar app.jar
配置冷启动类加载耗时
JDK 21 + AppCDS3240ms1810ms
JDK 24 AOT 缓存2604ms940ms

类加载耗时降了 48%,整体启动降 20%(相对 AppCDS)。单独看不算惊艳,但配合 GraalVM 用在最需要的地方,函数计算那块的月度成本降了 62%。

几个面向 AI 场景有用的小特性

除了上面三个大头,还有几个特性在我们的 AI 代码里用上了。

JEP 485:Stream Gatherers(正式特性)

这是 JDK 24 里唯一转正的语言/库级特性。它给 Stream API 增加了自定义中间操作的能力,我们在 RAG 的切片流水线里用得很顺手。

以前按 token 预算动态分批(每段不超过 512 token,但不要把一句话切断)要手写循环,现在可以这样:

// 按累计 token 数分块,且不切断句子
List<String> chunks = sentences.stream()
        .gather(Gatherer.ofSequential(
            AtomicInteger::new,
            (state, sentence, downstream) -> {
                int n = estimateTokens(sentence);
                if (state.get() + n > 512 && state.get() > 0) {
                    state.set(0);
                    downstream.push(current.toString());
                    current.setLength(0);
                }
                current.append(sentence);
                state.addAndGet(n);
                return true;
            }))
        .toList();

代码不算短,但比手写循环的状态管理清晰,而且能跟 Stream 的其他操作无缝组合。我们在文档切片、批量 embedding 请求分批、结果流式聚合三处都用了。

JEP 487 / 499:ScopedValue 和结构化并发(仍是预览)

这两个都还是预览特性(第四次预览),我们没有在生产用,但在内部工具里试过。

ScopedValue 对 AI 服务的价值很直接:链路追踪的 traceId、用户身份、租户信息这些需要跨层传递但不想污染方法签名的东西,用 ThreadLocal 在虚拟线程下有个问题——虚拟线程可能上百万个,每个都持有一份 ThreadLocal Map 副本,内存浪费。ScopedValue 是不可变的、有作用域的,更适合这个场景。

private static final ScopedValue<RequestContext> CTX
        = ScopedValue.newInstance();

void handle(Request req) {
    ScopedValue.where(CTX, buildContext(req))
               .run(() -> process());   // process() 内部任意深度都能读 CTX.get()
}

但预览特性要用得加 --enable-preview,而且每次 JDK 升级 API 都可能变。我们的策略是再等等,等它转正。目前链路追踪还是老老实实用 ThreadLocal + 显式清理。

JEP 489:Vector API(第九次孵化)

老实说,这个我没能用上。它的想法很好——用 SIMD 指令加速向量运算,理论上对 embedding 的余弦相似度计算、矩阵乘法这些有帮助。我们的相似度计算是向量数据库做的,不在 Java 侧,所以没机会用。

但我做过一个 benchmark,纯 Java 计算 768 维向量的余弦相似度,100 万次:

// 标量实现
for (int i = 0; i < dim; i++) dot += a[i] * b[i];

// Vector API(使用 FloatVector,128 位)
var species = FloatVector.SPECIES_PREFERRED;
for (; i < species.loopBound(dim); i += species.length()) {
    var va = FloatVector.fromArray(species, a, i);
    var vb = FloatVector.fromArray(species, b, i);
    acc = acc.add(va.mul(vb));
}
实现100 万次耗时
标量循环1240ms
Vector API385ms

3.2 倍提升,很可观。但这是第九次孵化了,至今没转正,而且 API 一直在变。如果你现在需要高性能数值计算,我建议用现成的库(比如 ND4J、ojAlgo),别赌这个 API。

升级过程遇到的坑

记录几个实际问题,给要升级的人参考:

  • Spring Boot 版本要够新。Spring Boot 3.4 官方支持 JDK 24,3.2 及以下没测过。我们有个服务在 3.2 上升 JDK 24,启动报 Unsupported class file major version 68,是因为里面的 Byte Buddy 版本太老;
  • 字节码增强类库要升级。Byte Buddy、ASM、CGLIB、Javassist 这几个都需要较新版本才能识别 JDK 24 的 class 文件版本(68)。我们的 SkyWalking agent 从 9.2 升到 9.4 才正常;
  • 移除了 Windows 32 位支持(JEP 479),对我们没影响,但如果你有相关构建流水线要留意;
  • Security Manager 彻底禁用(JEP 486)。我们有个老依赖在启动时会尝试设置 Security Manager,现在会直接抛异常。用 -Djava.security.manager=allow 可以临时绕过,但建议直接改代码;
  • 紧凑对象头会影响 JOL 类的内存分析工具。我们用 JOL 排查过一次对象布局,结果跟实际不一致,排查半天才发现是开了紧凑对象头。

关于"面向 AI 时代的演进"的一点看法

JDK 24 官方并没有把 AI 作为主题,但客观上,这一轮的很多改进恰好踩在 AI 应用的痛点上:

  • 虚拟线程解决的是 AI 服务高并发 IO 编排的问题,JEP 491 补上了最后一块拼图;
  • AOT 缓存解决的是 Serverless 场景下 AI 推理函数冷启动贵的问题;
  • 低延迟 GC + 紧凑对象头解决的是 AI 数据管道里海量小对象的内存效率问题。

这些改进没有一个是专门为 AI 设计的,它们本来就是 JVM 演进的自然方向。AI 应用恰好是"高并发 IO + 内存密集 + 弹性伸缩"的典型负载,把这些 JVM 的长期改进全部激活了。

我个人的判断是:Java 在 AI 时代的机会不在"用 Java 训练模型",而在"用 Java 承载 AI 系统"。而这件事能不能做好,取决于 JVM 在高并发、低延迟、快速启动这些传统指标上的表现,跟 AI 本身关系不大。从这个角度看,JDK 21 到 24 这一轮的改进,其实让 Java 在 AI 工程领域的竞争力变强了不少。

小结

四个服务升级 JDK 24,跑了四个月,没有出现因 JDK 版本导致的问题。最大的收益来自 JEP 491(P99 降 78%),其余是稳步改善。

给想升级的人的建议:

  • 如果用了虚拟线程,JEP 491 值得单独为此升级,这是 JDK 24 最有价值的一个改动;
  • 大量小对象 + 对延迟敏感的服务,可以试试分代 Shenandoah + 紧凑对象头,但要接受它们是实验特性;
  • 冷启动敏感的服务开 AOT 缓存,别忘记录制脚本要覆盖业务路径;
  • 核心链路先别动,非 LTS 版本的支持周期是真实风险;
  • 升级前检查所有字节码增强类库的版本,这是最常见的问题来源。

最后提醒一点:JDK 24 的免费支持窗口只有 6 个月。我们的计划是在下一个 LTS 发布后评估迁移,在那之前这几个服务保持每月关注安全公告。非 LTS 上生产是可以的,但一定要有明确的退出计划。

参考