Blog chevron_right Java

ReadyNow 如何缩短 Java 预热时间

How ReadyNow Improves Java Warmup Time

这是关于加快 Java 应用程序预热系列的第二篇博文。第一篇博文更快的 Java 预热:CRaC ReadyNow解释了 CRaC ReadyNow 如何使用不同的方法实现更快的 Java 预热。这篇文章深入探讨了 ReadyNow 如何缩短 Java 预热时间并减少延迟。

OpenJDK 的 Azul Zing 构建版本 (Zing) 中的 ReadyNow 可帮助 Java 应用程序在 JVM 启动后更快地达到最佳性能。使用包含上次运行的编译器决策历史记录的文本文件,可以缩短应用程序的总体 Java 预热时间。

在这篇文章中,我们将深入研究这一过程,并解释 ReadyNow 如何从一开始就提高应用程序的速度并减少延迟 [图 1]。

图 1:ReadyNow 缩短 Java 预热时间

ReadyNow 如何实现更快的编译

通常,要将方法从 Java 字节码编译为本机代码,需要执行多次。对于第 1 层和第 2 层编译器、所使用的运行时以及可选配置,阈值是不同的,但通常在 5,000 或 10,000 次以上。所有这些执行都需要时间,并且需要实际的生产负载。

凭借 ReadyNow,JVM 从上次运行中了解到这些方法已被编译,以及编译器做出了哪些决策来实现最佳本机代码。因此,它将主动启动编译,而不是等待所有阈值通过。这样,编译就可以更快开始,节省时间,并更快地达到预热状态。

立即阅读
关于 ReadyNow
ReadyNow 加快启动并保持快速运行
高性能 Java 平台基准报告

正常启动

使用 ReadyNow 的第一步是像平常一样使用 Zing JVM 和额外的参数运行您的应用程序

-XX:ProfileLogOut=<file>

这将指示 Zing 将每个编译器决策存储(输出)在配置文件日志文件中。

此示例还使用该参数生成垃圾收集器日志,以便在本文中进一步比较差异。当然,您可以使用自己的应用程序,但请确保您有某种方法来触发应用程序在生产环境中需要处理的预期负载。在本例中,我使用的是 DaCapo Benchmark Suite,它可以被指示自行生成特定负载,从而使我们能够轻松地比较具有相似负载的不同运行结果。

这只是一个示例用法。理想情况下,您应该使用您的生产应用程序,并让它处理实际需要处理的负载。您还需要让它运行更长时间,以确保最终获得一个包含所有必要信息的配置文件日志,这些信息基于应用程序从启动到最佳性能点的实际使用情况。

$ /usr/lib/jvm/zing-jdk21/bin/java \
   -XX:ProfileLogOut=readynow.log \
   -Xlog:gc:gc.log \
   -jar dacapo-23.11-chopin.jar h2

文件内容

生成的配置文件日志输出文件包含一些日志行,对于开发者或 DevOps 人员来说,可以通过检查文件本身的内容或使用 GC 日志分析器来获取有用的信息。例如,您可以检查此文件中的 Java 版本、参数和其他运行时信息。文件中还记录了与应用程序在预热期间的行为相关的已加载类和已编译方法,因此,当此文件用作输入配置文件日志时,ReadyNow 可以主动帮助 JVM 更快地完成预热。

# ZVM version:
# 21.0.5.0.101-zing_25.01.0.0-b5-release-linux-X86_64
# VM Arguments:
# jvm_args: -XX:ProfileLogOut=readynow.log -Xlog:gc:gc.log 
# java_command: dacapo-23.11-chopin.jar h2
# class_path: dacapo-23.11-chopin.jar
# java_home: /usr/lib/jvm/zing-jdk21.0.5.0.101-25.01.0.0-5-amd64
...
# ProfileLogOut after macro expansion: 'readynow.log'
...
# GCHH : OS name       : Linux 
# GCHH : OS release    : 6.8.0-53-generic #55-Ubuntu SMP PREEMPT_DYNAMIC Fri Jan 17 15:37:52 UTC 2025 
# GCHH : hardware      : x86_64 
# GCHH : CGroups information: 
# GCHH :   Container Type                   : cgroupv2 
# GCHH :   Active processor count           : 16 
...
1740141511794777793 | FirstCall 8512 5 0
Class 877 1260777173 org.dacapo.harness.TestHarness 9 { } 272310769 Class 901 file:/dacapo-23.11-chopin.jar
1740141511795023287 | ClassLoad 877
ImplementorLoad 877 1 877 

在某些情况下,Azul 支持工程师还会使用此文件协助客户调试特定的性能、编译和预热问题。应用程序性能图表(响应时间、吞吐量)可以与这些配置文件日志结合使用,以比较使用和不使用 ReadyNow 的结果。例如,根据客户关注的指标,它可以帮助判断是否可以使用更少的集群节点实现相同的系统性能。

使用文件启动

在第二步中,我们启动相同的应用程序,但使用第一步中生成的配置文件,此时将它与 -XX:ProfileLogIn=<file> 参数(输入)一起使用。这将指示 Zing 根据编译器在第一次运行中做出的判断,立即开始将 Java 类文件编译为原生代码。

$ /usr/lib/jvm/zing-jdk21/bin/java \
   -XX:ProfileLogIn=readynow.log \
   -Xlog:gc:gc-with-profile.log \
   -jar dacapo-23.11-chopin.jar h2

比较不同的运行结果

Zing 生成的 gc.log 文件包含的信息比 OpenJDK 构建版本生成的类似文件多得多,尤其是有关编译器行为的信息。因此,我们可以推断出编译器在运行过程中的工作情况。其他 OpenJDK 运行时无法提供如此详细的信息。

让我们使用 GC 日志分析器比较两次运行期间生成的 gc.log 文件。

第 1 层编译计数

在第一次运行中,我们可以看到编译次数随着时间的推移逐渐增加,并在大约 40 秒后达到最大值。得益于配置文件日志文件中的信息,在第二次运行中,编译器已经包含了所有必要的信息,可以将这些第 1 层编译移至应用程序的最开始,并在一秒钟内完成所有编译。

第 2 层编译计数

第 2 层编译的图表显示,我们将大部分编译时间从 40 秒缩短到了 17 秒。尽管这已经是一项显著的改进,但它也帮助我们明确了生成的配置文件日志文件仍然没有针对此用例进行完美的训练。在本博文系列的下一部分内容中,我们将进一步探讨分代配置文件日志的主题。

编译器队列运行

显示代码在队列中等待编译情况的图表也体现出了巨大差异。在第一次运行中,我们可以看到从大约第 26 秒(Dacapo 测试实际启动时)开始出现一个巨大的峰值,这给应用程序带来了很大的负载。得益于配置文件日志,该队列已移至应用程序的起始位置。如前所述,ReadyNow 会根据前一次运行的决策立即编译大部分原生代码,并在需要时提供。因此,我们仅在 26 秒处看到一个小峰值。

结语

凭借应用程序“训练运行”期间生成的配置文件日志,我们可以在应用程序启动之初将编译从 Java 字节码转移到性能最佳的原生代码。这样可以非常快速地预热应用程序的新实例,从而帮助您动态扩展系统并在启动后快速以最大速度运行。
下一篇:如何训练 ReadyNow 以实现最佳性能