简单有效的 G1 GC 调优技巧

G1 GC 是一种自适应垃圾回收算法,其自 Java 9 以来就是默认的 GC 算法。我们想分享一些有关 G1 垃圾回收器调优的小建议,希望能帮助你实现更优秀的程序性能。

1. 最大 GC 停顿时间

考虑通过“-XX:MaxGCPauseMillis”参数传递偏好的停顿时间目标。本参数可用于设置最大停顿时间目标值,G1 GC 算法会尽量尝试去达成这一目标。

2. 避免设置 Young 代大小

避免将 Young 代大小设置为特定值(可通过传递“-Xmn, -XX:NewRatio”参数实现)。G1 GC 算法会在运行时修改 Young 代大小以实现其停顿时间目标。如果显式配置了 Young 代大小,那么停顿时间目标将无法实现。

3. 清除旧的参数

在从其他 GC 算法(CMS、Parallel…)转向 G1 GC 算法时,记得清除所有与旧 GC 算法相关的 JVM 参数。通常而言,将旧的 GC 算法参数传递给 G1 不会有任何效果,甚至有时会产生负面效果。

4. 消除字符串重复项

鉴于低效的编程实践方式,当代程序会浪费不少内存。本篇案例研究展示了 Spring Boot 框架的内存浪费情况。内存浪费的一个重要原因就是字符串重复。最近的一项研究表明,程序内存的 13.5% 都是重复的字符串。所以在传递“-XX:+UseStringDeduplication”时,G1 GC 提供了一个选项来消除重复字符串。如果程序运行于 Java 8 update 20 或更高版本,就可以考虑将此参数传递给程序。该选项中蕴藏着提升程序整体性能的潜力。您可在这篇文章中了解更多有关该属性的信息。

5. 了解默认设置

出于调优目的,我们在下表中总结了重要的 G1 GC 算法参数及其默认值:

G1 GC 参数描述
-XX:MaxGCPauseMillis=200设置最大停顿时间值。默认值为 200 毫秒。
-XX:G1HeapRegionSize=n设置 G1 区域大小。值必须为 2 的 N 次幂,如:256、512、1024…范围是 1MB 至 32MB。
-XX:GCTimeRatio=12设置应用于 GC 的总目标时间与处理客户事务的总时间。确定目标 GC 时间的实际公式为 [1 / (1 + GCTimeRatio)]。默认值 12 表示目标 GC 时间为 [1 / (1 + 12)],即 7.69%。这意味着 JVM 可将 7.69% 的时间用于 GC 活动,其余 92.3% 用于处理客户活动。
-XX:ParallelGCThreads=n设置 Stop-the-world 工作线程的数量。
如果逻辑处理器的数量小于或等于 8 个,则将 n 值设置为逻辑处理器的数量。如果您的服务器有 5 个逻辑处理器,则将 n 设置为 5。
如果有 8 个以上的逻辑处理器,请将该值设置为逻辑处理器数量的大约 5/8。这种设置在大多数情况下都有效,除了较大规模的 SPARC 系统——其中 n 值可以大约是逻辑处理器数的 5/16。
-XX:ConcGCThreads=n设置并行标记线程的数量。将 n 值设为并行垃圾回收线程(ParallelGCThreads)数的大约 1/4。
-XX:InitiatingHeapOccupancyPercent=45当堆内存使用率超过此百分比时会触发 GC 标记周期。默认值为 45%。
-XX:G1NewSizePercent=5设置用作 Young 代空间大小的最低堆内存百分比。默认值为 Java 堆内存的 5%。
-XX:G1MaxNewSizePercent=60设置用作 Young 代空间大小的最高堆内存百分比。默认值为 Java 堆内存的 60%。
-XX:G1OldCSetRegionThresholdPercent=10设置混合垃圾回收周期中要收集的 Old 区域数量上限。默认为 Java 堆内存的 10%。
-XX:G1ReservePercent=10设置需保留的内存百分比。默认为 10%。G1 垃圾回收器会始终尝试保留 10% 的堆内存空间空闲。

6. 研究 GC 原因

优化 G1 GC 性能的有效途径之一是研究触发 GC 的原因并思考减少此类原因的解决方案。以下是研究 GC 原因的步骤。

1. 在应用程序中启用 GC 日志。可通过在启动时向应用程序传递以下 JVM 参数来启用:

Java 8 及之前版本:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:{file-path}

Java 9 及之后版本:

-Xlog:gc*:file={file-path}

2. 可使用包含 GCeasyGarbage CatHP Jmeter 在内的免费工具来对 GC 日志文件进行分析。此类工具会报告触发 GC 活动的原因。下方是上传 G1 GC 日志文件后由 GCeasy 工具生成的 GC 原因表格。如需查看完成分析报告可前往此处

图:G1 GC 原因(节选自 GCeasy 报告)

以下是针对各类原因的解决方案。

6.1.Full GC – Allocation Failure

Full GC – Allocation Failure 的起因有两个:

  • 应用程序创建了过多对象且回收对象的速度跟不上。
  • 堆内存碎片化时,即使有大量可用空间,Old 代中的直接分配也可能失败

以下是该问题的可能解决方案:

  1. 通过设置‘-XX:ConcGCThreads’值提高并发标记线程数量。增加并发标记线程数量可让垃圾回收运行速度更快。
  2. 强制 G1 提前开始标记阶段。可通过降低‘-XX:InitiatingHeapOccupancyPercen’值实现。默认值为 45。其意义是 G1 GC 标记阶段只会在堆内存使用率达到 45% 时开始。降低该值会让 G1 GC 标记阶段提前触发,这样可避免 Full GC
  3. 即使堆内存中有足够的空间,但由于缺少连续空间,Full GC 也可能会发生。导致该问题的原因可能是内存中存在很多占用空间巨大的对象(参见本文 “6.3. G1 大型对象分配”)。解决此问题的一个潜在方案是通过选项“-XX:G1HeapRegionSize”来增加堆内存大小,从而减少大型对象所浪费的内存。

6.2.G1 疏散停顿或疏散失败

当您看到 G1 疏散停顿时,说明 G1 GC 没有足够的内存用于 Survivor 或已晋升对象。Java 堆内存无法得到扩展,因为其已经处于最大值。以下是该问题的可能解决方案:

  1. 增大‘-XX:G1ReservePercent’参数的值。默认值为 10%。这意味着 G1 垃圾收集器将尝试始终保持 10% 的可用内存。增大该值后,GC 将提前触发,防止疏散停顿的出现。
  2. 通过降低‘-XX:InitiatingHeapOccupancyPercent’来提前开始标记周期。默认值为 45。降低该值会让标记周期更早开始。当堆内存使用率超过 45% 时会触发 GC 标记周期。另一方面,如果标记周期提前开始但没有进行回收,请将‘-XX:InitiatingHeapOccupancyPercent’阈值增加到默认值以上。
  3. 您还可增大‘-XX:ConcGCThreads’参数的值,从而提高并行标记线程的数量。增加并发标记线程数量可让垃圾回收运行速度更快。
  4. 如果问题仍然存在,您可以考虑增加 JVM 堆内存大小(即:-Xmx)

6.3.G1 大型对象分配

任何超过一半区域大小的对象都可被认为是“大型对象”。如果区域中包含大型对象,则区域中最后一个大型对象与区域结尾之间的空间将不会被使用。如果多个这样的大型对象,那么未使用的空间就会导致堆内存碎片化。堆碎片的出现不利于应用程序性能。如果您看到多次大型对象分配,请提高‘-XX:G1HeapRegionSize’。其值应为 2 的幂次,范围是 1MB 至 32 MB。

6.4.System.gc()

当程序调用“System.gc()”或“Runtime.getRuntime().gc()”的 API 时,将触发 stop-the-world Full GC 事件。您可采用以下方式修复此问题:

a.搜索并替换

经典永不过时 :-)。在程序代码库中搜索“System.gc()”与“Runtime.getRuntime().gc()”。如果出现匹配,就将相应的内容移除。此方案适合“System.gc()”在您的程序源代码中触发的情形。如果“System.gc()”从第三方库、框架或外部源中调用,那么此方案将不起作用。在这种情况下,您可考虑使用 #b 和 #c 中所列出的选项。

b. -XX:+DisableExplicitGC

您可以强制禁用 System.gc() 调用。启动程序时传递 JVM 参数“-XX:+DisableExplicitGC”即可。该选项将停用所有来自程序堆栈的“System.gc()”调用。

c. -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses

您可传递 JVM 参数“-XX:+ExplicitGCInvokesConcurrent”。传递此参数时,GC 回收会与程序线程并发运行,进而缩短冗长的停顿时间。

d.RMI

如果您的程序使用了 RMI,则可控制其中进行“System.gc()”调用的频率。您可在程序启动时启用以下 JVM 参数来对频率进行配置:

-Dsun.rmi.dgc.server.gcInterval=n
-Dsun.rmi.dgc.client.gcInterval=n

这些属性的默认值为:

JDK 1.4.2 和 5.0 为 60000 毫秒(即 60 秒)

JDK 6 及更高版本为 3600000 毫秒(即 60 分钟)

您可将这些属性设置尽可能大的值以最大限度地减少影响。

如需了解更多有关“System.gc()”调用及其 GC 影响的信息,可参考本文

6.5. 堆内存转储发起的 GC

“堆内存转储发起的 GC”表明使用 jcmd、jmap、profilers 等工具从应用程序中捕获了堆内存转储文件。在捕获文件之前,此类工具通常会触发Full GC并导致长时间停顿。除非绝对必要,否则我们应当避免捕获堆内存转储文件。

结论

希望本文能给您带来一些帮助。希望您的程序调优工作能够带来最优性能。

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: