Blog chevron_right Java

深入了解 Java 应用程序的内存使用情况

Learn More About the Memory Use of Java Applications

总结

为 Java 应用程序确定理想的内存大小可能具有挑战性,但这对于实现最佳性能至关重要。使用垃圾回收器 (GC) 日志文件可以帮助确定 Java 应用程序所需的内存大小。GC 可以将 Java 应用程序的内存使用保持在尽可能低的水平,发现代码中的问题,并帮助为服务器或虚拟环境确定合适的规格。垃圾回收器日志记录提供了用于检查应用程序所需内存大小的正确指标

在本文中,您将了解:

  • 如何测试:应用程序开发与部署中的重要步骤
  • 使用 JMeter GUI 创建测试
  • Azul Zing OpenJDK Zulu 构建版本之间 GC 日志的差异

为服务器或虚拟实例确定运行 Java 应用程序的理想内存大小可能具有挑战性。但随着云实例的成本日益攀升,以及对生态的影响不断加剧,正确配置机器规格,从而在不进行过度配置的情况下处理预期负载变得非常重要,这样可以最大限度地降低机器成本并减少其生态影响。了解应用程序的内存大小需求对于以最低的运营成本实现最佳性能至关重要。

在本文中,我们将使用垃圾回收器 (GC) 日志文件确定应用程序所需的内存大小。得益于 Java 运行时,我们可以依赖 GC 清理不再使用的内存,并将整体内存占用保持在尽可能低的水平。在此过程中,GC 可以输出包含大量信息的日志文件,这些信息不仅有助于我们发现代码中的问题,还可以帮助我们为服务器或虚拟环境确定合适的规格。

相关文章
《性能测试中斜坡加载的最佳实践》讨论了必须让测试运行足够长的时间,以确保测量到应用程序在完全预热后负载情况的重要性。
《配置 Spring Boot 以使用 Azul Zulu 和调试选项构建 Docker 镜像》提供了有关在 Docker 镜像中使用 Azul Zulu 以及获取 JFR 和 GCLog 结果的更多信息。
Java 垃圾回收日志记录是否会影响应用程序性能?一篇介绍 GC 日志记录众多优势的文章。始终保留日志文件可让您了解许多信息。

如何测试您的应用程序

真实场景测试中最困难但也是最重要的部分,是拥有一个可重复的负载测试,并且该测试需要贴近应用程序的实际使用情况。这是应用程序开发和部署中的一个重要步骤,需要开发团队与 DevOps 团队展开协作。在设置测试以确定应用程序所需的内存量时,有几个要点需要注意。这些要点在其他场景中同样适用,例如测试最大吞吐量时。

  • 留足时间:当执行 Java 应用程序时,JVM 会将最常用的字节码(类文件)重新编译为本地代码。此过程需要一定时间(称为预热时间),因此您需要等待应用程序在预期的典型负载下运行足够长的时间。应用程序必须根据向其施加的负载,调用所有应执行的代码。
  • 谨慎对待本地测试:部分测试可在您自己的机器上轻松执行,但请注意测试本身带来的负载!在运行应用程序的同一台机器上执行负载测试,可能导致 CPU 和/或内存过载,从而影响被测试应用程序的性能。
  • 使用真实场景测试:只有当您能够在一个与生产系统相似的环境中模拟预期负载时,测试才有效。
  • 在生产环境中测试:GC 日志对系统性能的影响极小。在许多情况下,与搭建完整的测试环境相比,这是一种获取真实日志结果的更简单、更实惠的解决方案。

使用 Spring Petclinic 进行实验

我使用 Spring Pet Clinic 应用程序来为本文收集测试结果。源代码已在 GitHub 上提供,并包含一个 JMeter 测试脚本。

运行测试应用程序

要遵循此方法,请使用以下命令获取源代码、编译应用程序并启动它:

# 获取源代码
$ git clone https://github.com/spring-projects/spring-petclinic
$ cd spring-petclinic
 
# 生成 JAR 文件并运行,使用 CTRL+C 停止应用程序
$ ./mvnw package
$ java -Xlog:gc,safepoint:gc.log::filecount=0 -jar target/*.jar

您的应用程序现在已配置为将垃圾回收日志记录存储在单一文件中。这种设置非常适合本文中的测试。但在生产环境中启用 GC 日志时,应当使用滚动文件,以防止文件变得过大并占满存储空间。例如,使用 -Xlog:gc,safepoint:gc.log::filecount=10,filesize=100M 将日志轮转设置为最多 10 个文件,每个文件 100MB。如果未定义 filecount 和 filesize,则默认为 5 个文件,每个文件 20MB,因此 GC 日志最多只会使用 100MB。

关于 JMeter

Spring Petclinic 项目包含一个 JMeter 测试。此类测试可以使用 Apache JMeter 执行,这是一个 100% 纯开源的 Java 应用程序,旨在对功能行为进行负载测试并衡量性能。它最初为测试 Web 应用程序而设计,但后来扩展到了其他测试功能。请检查最新版本并下载

$ cd ~/Downloads
$ wget https://dlcdn.apache.org//jmeter/binaries/apache-jmeter-5.6.3.zip
$ unzip apache-jmeter-5.6.3.zip
$ rm apache-jmeter-5.6.3.zip

JMeter 测试可以通过 GUI 应用程序执行,但不建议这样做,因为这会带来 GUI 影响测试性能的风险。应该仅将 GUI 用于创建测试或运行测试以验证其配置。

使用 JMeter GUI 创建测试

  • 启动 Apache JMeter GUI 应用程序:
$ java -jar ~/Downloads/apache-jmeter-5.6.3/bin/ApacheJMeter.jar
  • 在用户界面中,点击“文件”>“打开”,选择文件 spring-petclinic/src/test/jmeter/petclinic_test_plan.jmx。
  • 您可以通过点击“开始”按钮来执行测试,以验证配置,这将启动线程模拟 500 位用户。
  • 让它运行至测试完成。活动线程数量将从 500 下降到 0。

在无界面模式下使用 JMeter 运行负载测试

在实际测试中,我们将以无界面模式运行 JMeter。在我的案例中,我在运行应用程序的同一台机器上执行测试,因为它有足够的内存和 CPU 来同时处理两者。采用相同方法时,请确保这对您的测试同样适用! 

让我们使用以下选项运行测试并生成报告:

  • -n:以无界面模式运行(无 GUI)
  • -t:要执行的 .jmx 测试脚本路径
  • -l:用于存储原始结果的 .jtl 文件路径
  • -o:负载测试完成后生成报告仪表板的输出文件夹路径,该文件夹必须为空
  • -e:在负载测试后生成报告仪表板
$ java -jar ApacheJMeter.jar -n -t spring-petclinic/src/test/jmeter/petclinic_test_plan.jmx -l jmeter.jtl -o jmeter-report/ -e

如果您未添加 -e 选项,仍可基于测试运行期间创建的 .jtl 文件稍后生成 HTML 报告

  • -g:测试期间生成的 .jtl 文件路径
  • -o:用于存储 HTML 报告的文件夹
$ java -jar ApacheJMeter.jar -g jmeter.jtl -o jmeter-report/

由于每一个新的 Java 运行时版本都会带来性能改进,因此了解生产系统中使用的是哪个版本非常重要。我使用 OpenJDK 的 Azul Zulu 构建版本 21.0.3 执行了测试。

$ java -version
#openjdk version "21.0.3" 2024-04-16 LTS
#OpenJDK Runtime Environment Zulu21.34+19-CA (build 21.0.3+9-LTS)
#OpenJDK 64-Bit Server VM Zulu21.34+19-CA (build 21.0.3+9-LTS, mixed mode, sharing)

读取 JMeter 报告

在 JMeter HTML 报告目录中(在我的示例中为 jmeter-report/,由 -o 参数指定),您可以找到一个包含 JMeter 测试结果的网站。在此处无法找到任何与内存相关的信息,仅包含 JMeter 测试文件中定义的测试结果。例如:响应时间百分位数、每秒命中次数的吞吐量等。

检查 GC 日志结果

gc.log 文件是深入了解我们应用程序内存使用情况的关键所在。借助 Azul GC 日志分析器,我们可以读取此文件,并以可视化方式查看一组随时间(挂钟时间和运行时间)变化的图表,以检查垃圾回收器、JIT 编译器、系统指标等。以下图表显示,在初始负载之后,垃圾回收器的暂停持续时间保持在 10 毫秒以下,垃圾回收后的堆内存大小维持在约 64MB。建议使用该值的两倍来为系统配置内存规格。因此,对于本例,在内存为 128MB 的情况下,应用程序可以处理测试期间生成的相同负载。

您可以对自己的应用程序采用相同的原则,并在更改 Java 运行时的 –Xmx 设置或虚拟环境的内存配置后,重新检查暂停持续时间和堆内存使用情况。

Azul Zing 与 OpenJDK 的 Zulu 构建版本之间 GC 日志的差异

通过另一个内部专用的基准测试,我们创建了一些额外的日志文件,用于展示 OpenJDK 的 Azul Zulu 构建版本和 Zing 构建版本(第 17 版)所提供的不同结果。

Zulu 的结果

当我们使用 Zulu(一个 OpenJDK 的构建版本)生成 GC 日志时,日志文件中的数据与大多数其他发行版相同。以下图表显示,垃圾回收器的暂停持续时间保持在 80 毫秒以下,且垃圾回收后,老年代中长生命周期对象的堆内存使用量约为 1 GB,新生代中临时对象的堆内存使用量约为 2 GB。在此特定测试案例中,-Xmx4G 的总量已经足够,且实际使用情况与之相符,但通常的标准建议是将 -Xmx 设置为所观察到堆内存使用量的两倍,即 -Xmx6G。

Zing 的结果

我们使用 Zing 重复了相同的测试,Zing 是基于 OpenJDK 的替代 Java 运行时,具备更优的 JIT 编译器 (Falcon) 以及额外的垃圾回收器(C4,持续并发压缩回收器)。由于 C4 垃圾回收器提供了额外的信息,图表看起来略有不同。对于并发 GC,当 GC 与应用程序并行活动时,其并发持续时间通常更为重要。它不会暂停应用程序,但会消耗一定的 CPU 时间。100% 并不意味着它消耗了全部 CPU 时间,因为基准 100% 是 GC 线程的总数,而该数量少于 CPU 核心数,但长时间保持在 100% 应通过增加堆内存大小来避免。其中大部分时间通常由 GC 用来处理临时对象。在此特定测试案例中,即使使用相同的 -Xmx4G,Zing 的应用程序性能仍优于 Zulu。在进行总体内存规格配置时,Zing 的“存活集”图表同样重要,因为它显示了存活对象的数量,即不包含未引用对象(也称为垃圾)。

结语

垃圾回收器日志记录提供了用于检查应用程序所需内存大小的正确指标。能够在与生产系统相同的环境中以相似的负载测试应用程序至关重要。也许“在生产环境中测试”是实现这一目标的最简单方法。

prime-cta-banner

希望将 GC 日志用于应用程序测试?