◀返回
优化原生可执行文件的内存占用
选择合适的垃圾收集器并调整垃圾收集配置可以减少 GC 时间和内存占用。当运行原生镜像时,Java 堆设置是根据系统配置和 GC 确定的。您可以覆盖默认配置,以进一步改善您的用例在相关指标上的表现。
本指南演示了如何在内存消耗方面优化应用程序,并在 GC 暂停时间、内存占用和性能之间进行权衡。
前提条件
请确保您已安装 Oracle GraalVM for JDK 23 或更高版本。最简单的入门方法是使用 SDKMAN!。有关其他安装选项,请访问下载部分。
1. 准备应用程序
一个进行大量文本处理(如日志分析)的 Java 应用程序,其中大字符串频繁地被连接、分割或操作,是压力测试垃圾收集器的好方法。
您将使用的应用程序会生成大量临时字符串,从而给 GC 带来压力。
- 将以下 Java 代码保存到名为 StringManipulation.java 的文件中
import java.util.ArrayDeque; public class StringManipulation { public static void main(String[] args) { System.out.println("Starting string manipulation GC stress test..."); // Parse arguments int iterations = 1000000; int numKeptAliveObjects = 100000; if (args.length > 0) { iterations = Integer.parseInt(args[0]); } if (args.length > 1) { numKeptAliveObjects = Integer.parseInt(args[1]); } ArrayDeque<String[]> aliveData = new ArrayDeque<String[]>(numKeptAliveObjects + 1); for (int i = 0; i < iterations; i++) { // Simulate log entry generation and log entry splitting. The last n entries are kept in memory. String base = "log-entry"; StringBuilder builder = new StringBuilder(base); for (int j = 0; j < 100; j++) { builder.append("-").append(System.nanoTime()); } String logEntry = builder.toString(); String[] parts = logEntry.split("-"); aliveData.addLast(parts); if (aliveData.size() > numKeptAliveObjects) { aliveData.removeFirst(); } // Periodically log progress if (i % 100000 == 0) { System.out.println("Processed " + i + " log entries"); } } System.out.println("String manipulation GC stress test completed: " + aliveData.hashCode()); } }
在运行时,您可以在命令行上指定此应用程序应运行多长时间(第一个参数,迭代次数)以及应保留多少内存(第二个参数)。
- 在 HotSpot 上编译并运行应用程序,并进行计时
javac StringManipulation.java
/usr/bin/time java StringManipulation 500000 50000
在一台拥有 48GB 内存、8 个 CPU 且 HotSpot 上使用默认 G1 GC 的机器上,结果应该相似,显示用户时间和运行时间、系统 CPU 使用率以及执行此请求所需的最大内存使用量
Starting string manipulation GC stress test... Processed 0 log entries Processed 100000 log entries Processed 200000 log entries Processed 300000 log entries Processed 400000 log entries String manipulation GC stress test completed: 1791741888 6.61user 0.57system 0:03.35elapsed 214%CPU (0avgtext+0avgdata 4046128maxresident)k 0inputs+64outputs (8major+39776minor)pagefaults 0swaps
结果显示,墙上时钟时间为 3.35 秒,总 CPU 时间为 6.61 秒 + 0.57 秒(表示实际 CPU 使用量),最大内存使用量为 3.85GB(驻留集大小,RSS)。
2. 使用默认 GC 构建原生镜像
现在,使用 Native Image 中默认的垃圾收集器(即 Serial GC)预编译此应用程序。Serial GC 是一种非并行、停止复制的 GC,专为低内存占用和小型 Java 堆大小而优化。
- 使用
native-image
构建native-image -o testgc-serial StringManipulation
-o
选项定义要生成的输出文件的名称。构建输出会在 初始化阶段 打印 GC 信息,即
[1/8] Initializing... ... Garbage collector: Serial GC (max heap size: 80% of RAM) ...
- 使用相同的参数运行原生可执行文件,并进行计时
/usr/bin/time ./testgc-serial 500000 50000
资源使用量现在有所不同
Starting string manipulation GC stress test... ... 8.82user 1.24system 0:10.10elapsed 99%CPU (0avgtext+0avgdata 611272maxresident)k 0inputs+0outputs (0major+854664minor)pagefaults 0swaps
当使用默认 GC 时,此基准测试显示与上述 HotSpot 运行相比,运行时间更长,但最大驻留集大小更小。
- 通过在运行时传入
-XX:+PrintGC
来打印日志,以获取有关此 GC 的更多信息/usr/bin/time ./testgc-serial 500000 50000 -XX:+PrintGC
请注意,Serial GC 的暂停时间很高,这对于延迟敏感的应用程序可能是一个问题。例如
[9.301s] GC(55) Pause Full GC (Collect on allocation) 400.19M->214.69M 318.384ms
在这里,GC 暂停应用程序 318.384 毫秒。
3. 使用 G1 GC 构建原生镜像
下一步是更改垃圾收集器。Native Image 通过向 native-image
构建器传递 --gc=G1
来支持 G1 垃圾收集器。G1 GC 是一种分代、增量、并行、大部分并发、STW(Stop-The-World)的 GC,建议用于改善应用程序的延迟和吞吐量。
G1 GC 可与 Oracle GraalVM 一起使用,且仅支持 Linux。
我们建议将 G1 GC 与 配置文件引导优化 (PGO) 结合使用,以获得最佳应用程序性能。但是,为了保持说明的简洁性,本指南中未应用 PGO。
- 使用 G1 GC 构建第二个原生可执行文件,并为输出文件指定不同的名称,这样可执行文件就不会相互覆盖
native-image --gc=G1 -o testgc-g1 StringManipulation
构建输出现在打印不同的 GC 信息
[1/8] Initializing... ... Garbage collector: G1 GC (max heap size: 25.0% of RAM)
- 使用相同的参数运行此原生可执行文件,同时传入
-XX:+PrintGC
以获取更多关于暂停时间的见解,并比较结果/usr/bin/time ./testgc-g1 500000 50000 -XX:+PrintGC
... Processed 300000 log entries [2.705s][info][gc] GC(16) Pause Young (Normal) (G1 Evacuation Pause) 2301M->1690M(4840M) 25.144ms Processed 400000 log entries [3.322s][info][gc] GC(17) Pause Young (Normal) (G1 Evacuation Pause) 2715M->1870M(4840M) 20.364ms String manipulation GC stress test completed: 305943342 5.77user 0.47system 0:03.85elapsed 161%CPU (0avgtext+0avgdata 3707920maxresident)k 0inputs+0outputs (0major+12980minor)pagefaults 0swaps
G1 GC 比 Serial GC 快得多,所以墙上时钟时间从 10.1 秒下降到 3.85 秒。暂停时间大大改善!但是,G1 GC 的内存使用量高于 Serial GC。
与上面(也使用 G1 GC 的)HotSpot 执行相比,性能处于同一水平,而内存使用量更低(3.68GB 对比 3.85GB),因为 Native Image 中的对象比 HotSpot 更紧凑。总 CPU 时间也更低。
4. 使用 Epsilon GC 构建原生镜像
Native Image 还支持另一种垃圾收集器:Epsilon GC。Epsilon GC 是一个无操作的垃圾收集器,它不执行任何垃圾收集,因此从不释放任何已分配的内存。此 GC 的主要用例是仅分配少量内存的极短运行应用程序。
Epsilon GC 仅应在非常特殊的情况下使用。我们建议始终将 Epsilon GC 与默认 GC (Serial GC) 进行比较,以确定 Epsilon GC 是否真的能为您的应用程序带来实际好处。
- 要启用 Epsilon GC,请在镜像构建时传递
--gc=epsilon
native-image --gc=epsilon -o testgc-epsilon StringManipulation
构建输出报告正在使用 Epsilon GC
[1/8] Initializing... ... Garbage collector: Epsilon GC (max heap size: 80% of RAM)
- 运行此原生镜像,但增加迭代次数
/usr/bin/time ./testgc-epsilon 3200000 50000
Starting string manipulation GC stress test... ... Processed 3100000 log entries Exception in thread "main" Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main" PlatformThreads.ensureCurrentAssigned() failed during shutdown: java.lang.OutOfMemoryError: Could not allocate an aligned heap chunk because the heap address space is exhausted. Consider re-building the image with compressed references disabled ('-H:-UseCompressedReferences'). Command exited with non-zero status 1 21.07user 13.11system 0:34.25elapsed 99%CPU (0avgtext+0avgdata 33556824maxresident)k 0inputs+0outputs (0major+8387698minor)pagefaults 0swaps
发生
OutOfMemoryError
异常是因为 Epsilon GC 不执行任何垃圾回收,并且堆在某个时候会满。您需要减少此应用程序的运行时间。使用结果与前几步中的结果不可比较,因为执行了更多工作(更多迭代)。
5. 构建原生镜像时设置最大堆大小
默认情况下,使用 Serial 或 Epsilon GC 时,原生镜像将其最大 Java 堆大小设置为物理内存的 80%;使用 G1 GC 时,设置为 25%。例如,在具有 16GB RAM 的机器上,使用 Serial 或 Epsilon GC 时,最大堆大小将设置为 12.8GB。但是,如果您在启用了压缩引用支持的 Oracle GraalVM 上运行,最大 Java 堆不能大于 32GB。此信息可以在每个构建的输出中找到。
要覆盖默认行为,您可以显式设置最大堆大小。有两种方法可以做到这一点。
5.1. 在运行时设置最大堆大小
第一种也是推荐的方法是使用默认堆设置构建原生镜像,然后在运行时使用 -Xmx
覆盖以字节为单位的最大堆大小。使用 Serial G1 和 G1 GC 原生镜像测试此选项。
- Serial GC
/usr/bin/time ./testgc-serial -Xmx512m 500000 50000
Starting string manipulation GC stress test... ... 9.53user 1.40system 0:10.99elapsed 99%CPU (0avgtext+0avgdata 590404maxresident)k 0inputs+0outputs (0major+953535minor)pagefaults 0swaps
- G1 GC
/usr/bin/time ./testgc-g1 -Xmx512m 500000 50000
Starting string manipulation GC stress test... ... 14.99user 0.41system 0:05.13elapsed 300%CPU (0avgtext+0avgdata 554004maxresident)k 0inputs+0outputs (0major+5622minor)pagefaults 0swaps
5.2. 在构建时定义最大堆大小
第二种方法是构建原生镜像并使用 -R:MaxHeapSize
选项为最大堆大小设置新的默认值。除非在运行时通过传递 -X...
或 -XX:...
选项显式覆盖,否则此默认值将在运行时使用。
- 创建一个新的原生可执行文件
native-image --gc=G1 -R:MaxHeapSize=512m -o testgc-maxheapset-g1 StringManipulation
注意更新后的 GC 信息
[1/8] Initializing... ... Garbage collector: G1 GC (max heap size: 512.00MB)
- 以相同的负载运行它
/usr/bin/time ./testgc-maxheapset-g1 500000 50000
在此测试机器上,结果应与步骤 5.1 中的先前数字匹配
Starting string manipulation GC stress test... ... 14.87user 0.44system 0:05.33elapsed 287%CPU (0avgtext+0avgdata 552292maxresident)k 0inputs+0outputs (0major+5694minor)pagefaults 0swaps
除了
-Xmx
,还有许多其他 GC 特定选项可供专家用于性能调优,例如-XX:MaxGCPauseMillis
用于设置目标最大暂停时间。在参考文档中查找完整的性能调优选项列表。
总结
选择正确的垃圾收集器并配置合适的垃圾收集配置可以显著减少 GC 暂停并提高应用程序的整体响应能力。您可以实现更可预测的内存使用,帮助您的原生应用程序在不同工作负载下更高效地运行。本指南提供了根据您的应用程序目标选择最佳 GC 策略的见解:低延迟、最小内存开销或最佳性能。