JDK 26 性能改进详解:LazyConstant、G1双卡表、AOT缓存等核心优化


JDK 26 性能提升全解析:从类库到垃圾回收器的优化清单!

JDK 26 给 Java 平台带来了超过一千项增强,性能提升覆盖了类库、垃圾回收器、编译器和运行时四个大块。

  • 类库里新增了 LazyConstant,让你能把对象初始化从启动时推迟到真正使用时,既保留线程安全又让 JVM 能像处理 final 常量一样做优化,启动更快、无用功更少。
  • MemorySegment::getString 去掉了内部临时数组,短字符串提取延迟明显下降。记录类的自动 hashCode() 现在跟手写版本一样快,用记录做 Map 键或 Set 元素时查找吞吐量能提上来。
  • 加密算法里 AES、ML-DSA 和椭圆曲线 P-256 都做了底层优化,CPU 专属指令用得更充分。
  • G1 垃圾回收器通过加第二张卡表,把应用线程和后台优化线程的同步去掉,引用多的负载吞吐量能涨 5% 到 15%,写屏障指令从 50 条减到 12 条左右。
  • AOT 对象缓存现在跟任何垃圾回收器都能配合,包括 ZGC,启动和预热更快。默认初始堆大小改成了最小堆,启动时少准备很多堆元数据,程序能更快开始干活。
  • C2 编译器能处理参数特别多的方法了,循环向量化的成本模型也更聪明,该向量化才向量化,不该做就不做。
  • 虚拟线程在等类初始化的时候会主动让出载体线程,不会堵着别的虚拟线程。

类库里的新家伙 LazyConstant 怎么省启动时间

LazyConstant 这个 API 在 JDK 25 的时候叫 StableValue,当时还带着 orElseSetsetOrThrowtrySet 这些方法。JDK 26 重新设计之后改名成 LazyConstant,去掉了那些设置方法,改用工厂方法 LazyConstant.of(() -> compute()),算出来的值还不允许是 null。这么改的好处是运行时模型变简单了,跟不可变集合和 ScopedValue 的风格对齐,用起来更顺手。

以前你想写一个懒加载的单例服务,通常得自己搞一个可变字段,加上 volatile 或者 synchronized 做双重检查锁,代码写起来啰嗦还容易出错。LazyConstant 就是来替换这种 "可变加空检查加同步" 的老模式的。

你只要把 LazyConstant.of(Service::new) 定义成一个 static final 字段,然后到处调用 get() 就行,初始化最多发生一次,多线程抢着调用也安全。

java
import java.lang.LazyConstant;

final class Application {
    private static final LazyConstant SERVICE = LazyConstant.of(Service::new);

    static Service service() {
        return SERVICE.get();
    }
}

底层原理还是靠 JVM 对 "稳定字段" 的支持,只要这个 LazyConstant 本身存在 final 字段里,值一旦被设置好,后续访问就能被优化得像访问 final 常量一样激进。这就给了你一个中间选择:既不是启动时把所有东西都初始化好,也不是每次访问都做一堆检查,而是推迟干活,但干完活之后享受常量级别的速度。

类库里还有一个顺手改进是把懒加载集合的工厂方法直接放到了 ListMap 接口上,比如 List.ofLazyMap.ofLazy,你不用再跑去别的工具类里找,写代码的时候更容易发现这个功能。

MemorySegment 取字符串不再偷偷造数组

MemorySegment::getString 这个方法是从内存段里读出一个 Java 字符串。以前它内部会先造一个临时数组,再把数组里的内容拷贝到新字符串里。那个临时数组是个中间产物,用完就扔。按理说 JIT 编译器应该能看出来这个数组没必要存在堆上,直接把它优化掉,也就是所谓的 "分配消除"。

但边界测试案例发现,JIT 并没有把这个临时数组的分配消除掉,也就是说每次调用 getString 都实实在在地在堆上分配了一个数组,然后再把它回收掉。对于短字符串来说,分配和回收的开销可能比实际拷贝数据还大,这就亏了。

JDK 26 改了这个实现,从内存段里提取字符串的时候,中间分配和拷贝的步骤减少了。早期基准测试显示,各种长度的字符串延迟都降低了,短字符串的改进尤其明显。如果你的热点路径上经常要把本地内存或堆外内存的数据转成 Java 字符串,这个改进就能降低每次调用的延迟,减少分配压力,间接让垃圾回收器少干活。

记录类的 hashCode 自动生成版本跟手写一样快

记录类在 Java 里用得很普遍,尤其是当 Map 的键或者 Set 的元素的时候,hashCode() 方法会被频繁调用。以前记录类自动生成的 hashCode() 性能不如手写的好,因为类型剖析信息没有完全用上。

JDK 26 修复了这个问题,让自动生成的 hashCode() 也能享受到类型剖析带来的优化,性能跟手写实现一样好。如果你的代码大量依赖记录来做查找、分组、索引或者去重,这个改进就能直接提高吞吐量。

加密算法底层算术和 CPU 指令都优化了

JDK 26 在加密这块做了好几个针对性的性能提升,涉及 AES、ML-DSA 和椭圆曲线 P-256。这些改动减少了密钥设置阶段的不必要工作,改进了底层大整数算术运算,还增加或增强了 CPU 专属的内联函数,让支持这些指令的 CPU 在执行常见加密操作时跑得更快。具体可以看 JDK-8371820、JDK-8371450、JDK-8371259 和 JDK-8365581 这几个 issue 的详情。

其他类库小改进攒起来也不少

GZIPInputStream 在读单个压缩流的时候性能提高了,比如从字节数组或者 socket 里收数据解压,现在更快了。Charset 字符集编码器也换上了懒加载常量的新 API,替代了旧的初始化模式。java.lang.reflect.Method::equals 加了一个快速路径,如果传进来的是同一个实例就直接返回 true,动态代理实现里方法分发的时候经常做相等性检查,这个改进能省一点开销。

G1 垃圾回收器加第二张卡表减同步

G1 垃圾回收器用一张卡表来追踪跨区域的指针更新,应用代码里插入的写屏障负责维护这张表。有些负载更新引用特别频繁,卡表在暂停阶段扫描起来就特别耗时。G1 本来会在后台优化卡表,但优化线程需要跟应用线程同步,写屏障和优化逻辑就变复杂了,速度也慢了。

JDK 26 引入第二张卡表来解这个问题。应用线程永远只更新 "活跃" 的那张表,不用做同步,写屏障变简单了。优化线程独立处理另一张表,那张表一开始是空的。当 G1 预测扫描活跃表会超过暂停时间目标的时候,它就原子地交换两张表。应用线程接着更新新的活跃表,优化线程继续处理另一张,两边各干各的。

结果就是引用多的负载吞吐量能提高 5% 到 15%,就算引用更新很轻的负载也能提升大约 5%。x64 上的写屏障指令从大概 50 条缩减到 12 条左右,暂停时间也略有下降。内存开销上,多一张卡表大概占堆的 0.2%,1GB 堆对应大概 2MB 本地内存。

AOT 对象缓存现在跟任何 GC 都能搭

JDK 26 之前,HotSpot 的 AOT 缓存只跟特定垃圾回收器配合得好。JDK 26 让 AOT 缓存跟任何垃圾回收器都能一起工作,包括 ZGC。AOT 缓存把 Java 堆对象(比如 Class 对象以及它们引用的字符串和字节数组)以 GC 特定的内存格式存起来,JVM 启动时可以直接把缓存里的对象映射到堆里,启动速度就快。

但问题来了,不同垃圾回收器表示对象引用的方式不一样,有压缩指针和非压缩指针的区别,堆大小和区域布局规则也不一样。为了兼容这些不兼容的引用格式,JDK 26 加了一种可选的 GC 无关对象格式。两种格式各有各的取舍。

GC 特定的可映射格式在热启动时几乎瞬间完成,因为缓存很可能已经在文件系统缓存里了。GC 无关的可流式格式在冷启动时能更好地隐藏磁盘延迟,但通常需要额外的一个 CPU 核心来做流式处理或物化工作。

JDK 自己带了两种类型的基线 AOT 缓存,就算应用程序不提供自己的缓存,JVM 也能在映射和流式之间选择。JVM 训练后会根据启发式规则决定生成哪种格式。如果训练时用了 ZGC、-XX:+UseCompressedOops 选项或者堆大于 32GB,就选可流式的 GC 无关格式。如果训练时用了压缩指针,就优先选可映射的 GC 特定格式。想强制用 GC 无关流式格式的话,可以加 -XX:+AOTStreamableObjects 选项,就算同时指定了 -XX:+UseCompressedOops 也管用。

这个改动的效果是更多应用能在不改变垃圾回收策略的前提下享受到更快的启动和预热。

默认初始堆大小改成最小堆

以前你没设 -Xms-XX:InitialHeapSize 的时候,JVM 默认用 InitialRAMPercentage 来算初始堆大小,默认值是物理内存的 1.5625%,大概 1/64。大内存机器上这可能导致启动时准备一个相当大的堆。

JDK 26 不再用默认的 InitialRAMPercentage 了。如果你没指定初始堆大小,JVM 就直接从最小堆 MinHeapSize 开始。启动时初始化的堆元数据少了,程序能更快开始执行,后面需要更多内存的时候堆再慢慢长大。

C2 编译器能处理参数超多的方法

HotSpot 的分层编译模型里,代码一般先在解释器里跑,然后可能被 C1 快速编译来加速预热,变成热点之后再用 C2 做更激进的优化。以前 C2 有个限制,处理不了参数特别多的办法,参数太多就编译失败或者干脆不编译,代码就一直留在 C1 或解释器里跑。

JDK 26 解除了这个限制,C2 现在能处理参数非常多的方法了。更多热点代码能享受到 C2 的激进优化,吞吐量提高,CPU 开销降低,应用代码完全不用改。

C2 向量化成本模型更聪明

循环向量化能把标量循环转成 SIMD 风格的向量操作,一次处理多个值,速度能快很多。但向量化不是什么时候都划算,因为为了凑齐向量数据可能需要做额外的数据重排和打包操作,这些额外工作的开销有时候会超过向量化带来的收益。

JDK 26 增强了 C2 的向量化成本模型,让编译器在做循环向量化决策时更明智。该向量化的时候向量化,不该向量化的时候就不做,避免好心办坏事。

虚拟线程等类初始化时主动让出载体

多个虚拟线程同时碰上一个正在初始化的类时,它们都得等那个类初始化完。以前等待的时候虚拟线程会一直挂在自己的载体线程上不放,载体线程数量有限,被占着就干不了别的活。

JDK 26 在常见的类初始化路径上让等待的虚拟线程可以被抢占,主动让出载体线程。这样减少了不必要的载体阻塞,虚拟线程多的应用扩展性更好,类加载或初始化的突发高峰也不容易导致吞吐量下降或载体线程饿死。

赶紧试试 JDK 26 吧

JDK 26 从 2026 年 3 月就已经正式发布了,现在正是拿你自己的应用和负载来试试的好时候。测试或者计划迁移的时候一定记得量一量性能,跟你现在用的 JDK 版本比一比。要是发现什么倒退的现象,可以到相关的邮件列表里反馈。生产级负载的反馈能帮助 Java 平台持续改进。JDK 27 的性能改进工作也在进行中,以后有机会再详细聊。



总结

JDK 26 在类库、垃圾回收、编译器和运行时四大方面带来大量性能提升,包括 LazyConstant 懒加载常量、MemorySegment 字符串提取优化、记录类 hashCode 优化、G1 双卡表减少同步、AOT 缓存支持所有 GC、默认初始堆改为最小堆、C2 支持超大参数方法、向量化成本模型改进以及虚拟线程等待类初始化时主动让出载体。