JEP 515:提前进行方法分析

原文:JEP 515- Ahead-of-Time Method Profiling
作者:
日期:2025-10-26

作者伊戈尔·韦列索夫(Igor Veresov)和约翰·罗斯(John Rose)
负责人约翰·罗斯(John Rose)
类型特性
范围实现
状态已关闭 / 已交付
版本25
组件hotspot/compiler
讨论组leyden - dev@openjdk.org
工作量M
时长M
相关JEP 483:提前进行类加载和链接
评审人亚历克斯·巴克利(Alex Buckley)、丹·海丁加(Dan Heidinga)、弗拉基米尔·科兹洛夫(Vladimir Kozlov)
批准人弗拉基米尔·科兹洛夫(Vladimir Kozlov)
创建时间2024/02/01 20:40
更新时间2025/07/17 19:01
问题编号8325147

摘要

通过在 HotSpot Java 虚拟机启动时,即时提供应用程序先前运行生成的方法执行分析数据,来缩短预热时间。这将使即时编译器(JIT)能够在应用程序启动时立即生成本地代码,而无需等待分析数据的收集。

目标

  • 通过将初始方法执行分析数据的收集从生产运行转移到训练运行,借助 AOT 缓存 传递分析数据,帮助应用程序更快地预热。
  • 无需对应用程序、库或框架的代码进行任何更改。
  • 不对应用程序执行引入任何新的限制。
  • 不引入新的 AOT 工作流程,而是使用 现有的 AOT 缓存创建命令

动机

要真正了解一个应用程序的功能,我们必须运行它。

通过检查应用程序的源代码或类文件,我们可以对其行为得出一些简单结论,但我们无法确定它与高度动态的 Java 平台如何交互。这种不确定性的一个原因是,在没有 finalsealed 修饰符的情况下,任何类都可以随时被继承,因此一个方法可能会被多次调用,然后被重写,之后再也不会被调用。另一个原因是,新的类可以根据外部输入加载,以其作者甚至都无法预测的方式扩展应用程序的行为。程序的复杂性总能使静态分析失效。

在运行应用程序时,JVM 可以识别哪些方法执行重要工作,以及它们是如何执行的。为了使应用程序达到最佳性能,JVM 的即时编译器(JIT)必须找到不可预测的“热点”方法集,即那些消耗最多 CPU 时间的方法,并将它们的字节码编译成本地代码。(这就是“HotSpot JVM”名称的由来。)由于应用程序先前的行为是预测未来行为的良好指标,先前行为的总结可以使 JVM 的编译工作集中在真正重要的代码上。

自 JDK 1.2 以来,HotSpot JVM 会自动以“分析数据”的形式收集这种总结。对于任何给定的方法,分析数据会记录许多有用的事件,例如其字节码指令的执行次数以及遇到的对象类型。有了足够的分析数据,JVM 就有了统计依据来预测该方法的未来行为,从而为该方法生成优化代码。分析数据使 JVM 既能优化热点方法,又能避免优化冷点方法;这两个条件对于达到最佳性能都是必要的。

不幸的是,这里存在一个“先有鸡还是先有蛋”的问题:应用程序在其方法行为得到预测之前无法达到最佳性能,而方法行为在应用程序运行相当长一段时间之前无法得到预测。

JVM 目前通过在应用程序运行早期投入一些资源来收集分析数据来解决这个问题。在这个“预热期”内,应用程序运行得较慢,直到 JIT 能够将热点方法编译成本地代码。预热期过后,除非应用程序改变其行为模式,触发新的预热期,否则无需再编译更多方法。

我们可以通过更早地在应用程序的“训练运行”中收集分析数据来缩短预热时间。这将分析和预测行为的工作从应用程序的生产生命周期中转移出去。因此,应用程序在生产中的预热时间将仅由 JIT 编译成本决定,应用程序可以更快地达到最佳性能。

描述

我们扩展了由 JEP 483 引入的 AOT 缓存,以便在训练运行期间收集方法分析数据。就像 AOT 缓存目前存储 JVM 在启动时否则需要加载和链接的类一样,AOT 缓存现在还存储 JVM 在应用程序运行早期否则需要收集的方法分析数据。因此,应用程序的生产运行启动更快,达到最佳性能也更快。

训练运行期间缓存的分析数据不会阻止生产运行期间的额外分析。这一点至关重要,因为应用程序在生产中的行为可能与训练中观察到的行为不同。即使有缓存的分析数据,HotSpot JVM 在运行时仍会继续对应用程序进行分析和优化,融合 AOT 分析数据、在线分析和 JIT 编译的优势。缓存分析数据的最终效果是 JIT 更早运行且更准确,利用分析数据优化热点方法,从而使应用程序的预热期更短。JIT 任务本质上是并行的,因此当有足够的硬件资源时,预热的实际时间可以很短。

例如,以下是一个程序,虽然简短,但使用了 Stream API,因此会导致加载近 900 个 JDK 类。大约 30 个热点方法会在最高优化级别下编译:

import java.util.*;
import java.util.stream.*;
public class HelloStreamWarmup {
    // 根据传入的数字生成问候语
    static String greeting(int n) {
        // 创建包含问候语单词的列表
        var words = List.of("Hello", "" + n, "world!");
        // 对列表中的单词进行过滤和拼接
        return words.stream()
            .filter(w -> !w.contains("0"))
            .collect(Collectors.joining(", "));
    }
    public static void main(String... args) {
        // 多次调用 greeting 方法
        for (int i = 0; i < 100_000; i++)
            greeting(i);
        // 输出最终的问候语
        System.out.println(greeting(0));  // "Hello, world!"
    }
}
  

在没有分析数据的 AOT 缓存下,这个程序运行需要 90 毫秒。将分析数据收集到 AOT 缓存后,它运行需要 73 毫秒——性能提升了 19%。包含分析数据的 AOT 缓存额外占用 250 千字节,比不包含分析数据的 AOT 缓存多约 2.5%。

像这样的短程序只有很短的预热期,但由于及时准确的 JIT 活动,有了缓存的分析数据后预热会更快。出于同样的原因,更复杂且运行时间更长的程序也可能更快地预热。

替代方案

如果一个应用程序的行为具有很强的可预测性,以至于我们可以提前将其热点方法编译成本地代码,并且这样做能够使它在无需进一步 JIT 活动的情况下达到最佳性能,那么这种 AOT 代码比缓存分析数据更可取。我们打算在未来的工作中实现 AOT 编译。

然而,许多应用程序受益于 AOT 编译和 JIT 编译的结合,因为它们的行为无法被 AOT 编译器准确预测。因此,缓存的分析数据和缓存的 AOT 代码并非相互对立,它们将协同为各种应用程序提供最佳性能。部分 AOT 解决方案,即合理的 AOT 代码逐渐被优化更好的 JIT 代码取代,最终可能是最佳解决方案。JIT 最初可以不干预应用程序的运行,而是根据最新的分析信息,从容地生成最终完美的代码。

测试

  • 我们将为该特性创建新的单元测试。
  • 我们将在启用此特性的情况下运行现有的 AOT 缓存测试,并确保测试通过。

风险与假设

除了 JEP 483 中已经提到的风险之外,没有新的风险。

AOT 缓存的基本假设仍然成立:假设训练运行是良好的观察来源,当通过 AOT 缓存传递到生产运行时,将有利于该生产运行的性能。