博客

来自 ej-technologies 的产品新闻、更新与见解。

通过 RSS 阅读,通过 电子邮件 订阅,或在 上关注我们。

HotSpot 的反击:JEP 544 会终结 GraalVM 的启动性能垄断吗?

2026-09-21
作者:Ingo Kegel
HotSpot JVM 迎来了激动人心的时刻:JEP 544: Ahead-of-Time Code Compilation 刚刚被提议纳入 Java 28。它有望让 HotSpot AOT 缓存获得足以媲美 GraalVM 的启动性能,而且完全不必承受 GraalVM 的种种艰辛。但训练 AOT 缓存与 GraalVM 的预编译并不相同,那么两者究竟孰优孰劣?

AOT 缓存的来龙去脉

20 多年来,Java 一直在设法提升启动速度:从 Java 5 引入类数据共享(CDS)开始,到 Java 10 通过 AppCDS 纳入应用类,再到 Java 13 简化训练流程。然而,无论从数字还是采用率来看,这些改进都没有产生真正令人印象深刻的影响。

2022 年,Mark Reinhold 宣布了“Project Leyden”计划,旨在大幅改善 Java 的启动性能。时间快进到 Java 24,它包含了 JEP 483,即 HotSpot AOT 缓存。AOT 缓存沿用了 CDS 的思路,但增加了一种更通用的机制:在训练运行之后,还会存储类的已链接状态以及归档的堆对象。

Java 25 通过 JEP 514 让 AOT 缓存变得实用,又通过 JEP 515 增加了方法性能分析结果的缓存。如今,随着拟议中的 JEP 544 加入对已编译方法的缓存,Project Leyden 已进入最后冲刺阶段,即将兑现它为 Java 启动性能所承诺的杀手级改进。

到底能快多少?

与大多数性能改进一样,实际数字会因应用的具体工作而有很大差异。一个很好的真实例子是 install4j IDE:它需要在启动时加载 1 万个类,然后才能显示主窗口。在我的 MacBook 上,这以前大约需要 3 秒钟。借助 Java 25 的 AOT 缓存,它 1.5 秒即可启动完成。速度翻倍是一项了不起的成就,对用户体验意义重大——从“迟缓”直接跃升为“敏捷”。

到了 Java 28,我们还将迎来又一次这样的改进,而且只需升级 JDK 即可。对复杂的 IDE 来说,亚秒级的启动速度实在快得惊人,“Java 启动慢”的旧有偏见必须被永远摒弃了。下面是 JEP 中展示的性能图表:

博客插图博客插图

这着实令人震撼,也凸显出以往的情况有多糟糕。

那 GraalVM 呢?

GraalVM 起源于多语言 VM,并曾作为 HotSpot C2 编译器的替代品,如今已演变为面向 JVM 字节码的原生镜像生成器。其生态系统已被广泛采用,Quarkus、Helidon 和 Spring Boot 都在其列。

使用 GraalVM,你可以得到一个无需 JVM 的单一二进制文件,而且无论是启动还是实际负载,都能获得出色的性能。

既然如此出色,为什么不是人人都在用呢?和许多好东西一样,它有一个限制,那就是封闭世界(closed world)要求:程序所能做的一切都必须在构建时已知。各种动态行为都必须提前声明:反射、动态代理、资源、序列化,不一而足。此外,GraalVM 原生镜像完全不支持 Swing 应用(install4j IDE 就是其中之一)。

来比较一下两者

为了这次对比,我特意挑选了一个偏向 GraalVM 的应用:BetterBranch 是一个精巧的命令行工具,用于报告所有本地 Git 分支相对当前分支的领先/落后状态,并且它以 GraalVM 二进制文件的形式发布。

以下是使用 hyperfine 在 Linux 上测得的数字。“真实负载”一列显示 BetterBranch 在其自身仓库上的运行情况;“快速退出”则是在没有 Git 仓库的目录中运行该工具,因此它会立即带着错误消息退出。

变体 快速退出 真实负载
纯 JVM 29.8 ms 280.5 ms
不含 AOT 代码的 AOT 缓存 25.2 ms 195.1 ms
含 AOT 代码的 AOT 缓存(JEP 544) 26.1 ms 131.6 ms
GraalVM 原生镜像 3.4 ms 50.1 ms

JEP 544 尚未包含在 JDK 28 的早期访问构建中,因此 AOT 代码的数据是用 Project Leyden 早期访问构建测得的。

在真实负载一列中,JVM 被固定为 2 个核心,因此 JIT 编译器必须与应用争抢 CPU。这正是 JEP 544 性能图表所用的测量环境,也是缓存的 AOT 代码真正发挥影响之处:与不含 AOT 代码的缓存相比,它节省了 33% 的墙钟时间;与纯 JVM 相比则节省了 53%。

柱状图,以纯 JVM 的百分比对比 BetterBranch 真实负载的墙钟时间:不含代码的 AOT 缓存为 69.6%,含 JEP 544 的 AOT 缓存为 46.9%,GraalVM 原生镜像为 17.9%柱状图,以纯 JVM 的百分比对比 BetterBranch 真实负载的墙钟时间:不含代码的 AOT 缓存为 69.6%,含 JEP 544 的 AOT 缓存为 46.9%,GraalVM 原生镜像为 17.9%

那么,差距已经很小了吗?不,GraalVM 仍然遥遥领先,但差距正在大幅缩小。如果容器化应用的启动时间需要精打细算到毫秒,就选择 GraalVM。如果你的应用无法应对封闭世界的种种限制,就使用 AOT——它在 Java 25 中已经可用,到 Java 28 还会更加惊人。

如何训练?

关于 AOT 缓存,还有一个问题悬而未决:在实践中究竟该如何创建缓存?如果应用只在内部部署,你可以在构建系统或部署脚本中执行一次训练运行。但如果是要对外发布的应用,又有哪些选择呢?你必须在应用实际运行所在的系统上进行训练。

对桌面应用来说,在客户的机器上安排一次训练运行听起来相当不切实际。你需要为训练运行添加一个特殊的 VM 参数,再为后续运行使用已录制的 AOT 缓存而添加另一个不同的参数。虽然 AOT 缓存带来的提速对桌面应用的用户体验非常重要,但强迫用户参与这样一套繁琐流程实在缺乏吸引力。

这时,install4j 的 AOT 缓存功能就派上用场了:install4j 启动器可以在应用首次启动时选择性地录制 AOT 缓存,只需将 -XX:AOTCacheOutput=<cache file> 作为 VM 参数传入即可。在后续运行中,则通过 -XX:AOTCache=<cache file> 来使用该缓存。

默认情况下,缓存存储在每用户缓存目录中。缓存文件名包含一个由应用构建和 Java 运行时派生出的键,因此在应用更新或 Java 运行时变更之后,缓存会自动重新生成。先前构建的缓存会被自动清理,卸载程序则会移除所有已创建的缓存。

如果想以编程方式结束训练运行,从 Java 25.0.4 起可以使用 HotSpotAOTCacheMXBean MBean,在其上调用 endRecording() 即可,例如在用户界面显示完毕且初始后台任务完成之后调用。

install4j 启动器向导中的 AOT 缓存步骤install4j 启动器向导中的 AOT 缓存步骤

对于命令行应用,在安装程序中安排一次带快速退出路径的训练运行是最佳选择,因为它能避免向用户展示训练过程中目前不可避免的终端输出。install4j 命令行编译器就是这样做的:以 --version 标志执行它。

结论

为了展示 JVM 的最坏情形,我特意挑选了一个让数字偏向 GraalVM 的例子。之所以如此,是因为这个应用非常小。在更大的应用上,GraalVM 无需启动 JVM 的优势就不那么重要了。当应用中真正工作的部分占比增大时,这部分固定收益会相应缩小,AOT 代码缓存将迎头赶上。能追上多少,完全取决于你测量的是哪个应用。以我们的 install4j IDE 为例,仅不含 AOT 代码的 AOT 缓存就已经将启动时间缩短了 50%,而前面那个更小的命令行应用约为 30%。

显而易见的是,借助 AOT 缓存的 JVM,其启动时间与 GraalVM 相比正变得更具竞争力,尤其是考虑到 GraalVM 所要求的种种限制。Java 25 现在已经很不错了,而在这方面,Java 28 还会更上一层楼。

如果你在发布软件,并希望借助 install4j 的 AOT 缓存录制功能来提升启动速度,欢迎试用,无需注册。

归档