0%

G1垃圾回收器调优

本文翻译自:Garbage-First Garbage Collector Tuning

关于G1的一般性建议

一般情况下,建议使用G1的默认设置,如果有其他需求,再设置期望停顿时间,并且通过-Xmx设置最大Java堆。

与其它回收器相比,G1默认设置下的平衡策略有所不同。在G1的默认配置中,它的目标既不是最大化吞吐量,也不是最小化停顿时间,而是在高吞吐量的同时提供相对较低且一贯的停顿时间。然而,G1用于增量回收的机制和对于停顿时间的控制,都在应用程序的线程和空间回收的效率方面都产生了一些开销。

如果更倾向于高吞吐量,请使用 -XX:MaxGCPauseMillis 放宽期望停顿时间或提供更大的堆。而如果主诉求是低延迟,则调整期望停顿时间。要避免使用 -Xmn-XX:NewRatio 等选项将年轻代、限定为固定值,因为(动态调整)年轻代大小是 G1满足停顿时间的主要手段。而将年轻代年轻代固定实际上会禁用停顿时间的控制。

从其他回收器迁移到 G1

通常情况下,当从其他垃圾回收器(尤其是并发标记清除(CMS)回收器)迁移到 G1 时,首先要移除所有影响垃圾回收的参数,仅需要设置期望停顿时间,作为总堆大小的-Xmx, 以及可选参数 -Xms

在特定方式下,许多参数在其他回收器中很有用,但是到了G1中,有的根本没有效果,有的甚至会降低吞吐量和或降低G1实现预期停顿的概率。例如设置年轻代大小会完全禁用 G1动态调整年轻代大小,而这是G1实现停顿时间特性的重要手段。

提高 G1 性能

G1 旨在提供良好的整体性能,而无需设定其他参数。但是,在某些情况下,G1的默认启发式配置或默认配置无法提供最佳性能。本节提供了在这些情况下进行诊断和改进的操作指南。本指南仅描述 G1 在给定应用下,给定指标中提高垃圾回收器性能的可能性。根据具体的场景,应用级的优化可能比尝试调优 VM更有效。例如可以可以减少短命对象的使用来规避某些异常的场景。

为了方便诊断,G1 提供了全面的日志记录。一个好的开始是使用 -Xlog:gc*=debug 参数,然后在必要时优化输出。该日志提供了垃圾回收活动的暂停期和非暂停期的详细概述。也包括垃圾回收的类型以及每个阶段耗时的细分统计。

以下小节会探讨一些常见的性能问题。

观察完整的垃圾回收

全堆垃圾回收(Full GC)通常非常耗时。老年代堆占用过高导致的Full GC可以通过在日志中查找Pause Full (Allocation Failure)关键字来确定。Full GC 之前通常会有转移失败导致的垃圾回收,这次垃圾回收包含to-space exhausted 标签(tags)。

发生 Full GC 的原因是应用程序分配了太多对象,并且这些对象不能被足够快地回收。这种情况下,并发标记不能及时完成以启动Space-reclamation阶段。如果此时分配了许多大对象,那么导致Full GC 的可能性可能会更加复杂。鉴于这些对象按照 G1 的方式分配,它们可能会占用比预期更多的内存。

确保并发标记按时完成是避免Full GC的目标。可以通过降低老年代的分配速率来实现,或者给并发标记更多的时间。

G1 提供了几个参数来更好地处理这种情况:

  • 可以使用 gc+heap=info 日志记录确定 Java 堆上大对象占用的Region数量。日志Humongous regions: X->YY 为提供大对象占用Region的数量。如果这个值高于老年代Region的数量,最佳选择是尝试减少该大对象的分配。(也)可以尝试使用 -XX:G1HeapRegionSize 参数增加Region的大小。当前堆的Region大小显示在日志的开头。
  • 增加 Java 堆的大小。这通常(也)会增加(并发)标记完成的时间。
  • 显式设置 -XX:ConcGCThreads 来增加并发标记线程的数量。
  • 强制 G1 提前开始并发标记。 G1 根据前面应用的行为自动推断初始堆占用百分比(Initiating Heap Occupancy Percent (IHOP))阈值。如果应用程序行为发生变化,那这些预测就可能是错误的。这是有两个选择: 通过修改 -XX:G1ReservePercent 增加自适应 IHOP 计算时使用的缓冲区,从而降低决定开启Space-reclamation的目标占用率;使用 -XX:-G1UseAdaptiveIHOP-XX:InitiatingHeapOccupancyPercent 手动设置来禁用 IHOP 的自适应计算。

除了分配失败(Allocation Failure )导致Full GC,通常也会有应用程序或某些外部工具导致Full GC。如果是由于调用了 System.gc(),并且无法修改应用的代码,可以使用 -XX:+ExplicitGCInvokesConcurrent 缓解 Full GC 的影响,或者通过设置 -XX:+DisableExplicitGC 让 VM 完全无视System.gc()。外部工具可能仍会强制Full GC,这时只能寄希望于别主动触发了。

大对象碎片

由于需要为大对象找到一组连续的Region的原因,因此可能会在 Java 堆内存耗尽之前发生 Full GC。在这种情况下,候选方案是通过参数:-XX:G1HeapRegionSize 增加堆Region大小以减少大对象的数量,或者增加堆的大小。在极端情况下,可能没有足够的连续空间可供 G1 分配对象。如果 Full GC 也无法回收出足够的连续空间,这将导致 VM 退出。因此,除了如前所述减少大对象的分配或增加堆内存外,没有其他选择。

停顿调优

本节讨论在常见停顿问题(即停顿时间太长)的情况下改进 G1 行为的建议。

异常的SystemReal 占用

对于每次垃圾回收暂停,gc+cpu=info 日志会输出一行,其中包含有关暂停时间耗时的OS信息。例如: User=0.19s Sys=0.00s Real=0.01s

User时间是在 VM 代码中的耗时,Sys时间是在OS中的耗时,而Real为垃圾回收暂停的绝对时间量。如果Sys时间相对较高,那么大多数情况下是环境造成的。

已知常见的高Sys时间包括:

  • VM 从操作系统内存分配或归还内存可能会导致不必要的延迟。通过使用选项 -Xms-Xmx 将最小和最大堆设置为相同的值来避免该延迟,并使用 -XX:+AlwaysPreTouch 将VM预先分配所有内存的工作移至启动阶段。
  • 特别是在 Linux 中,通过透明大页(*Transparent Huge Pages (THP)*)功能将小页面合并为大页面往往会随机停滞进程,这不仅限定在GC暂停期间。由于 VM 分配和维护了大量内存,因此 VM 被选为长时间停滞的进程的风险比通常高。关于如何禁用透明大页的信息,请参阅操作系统相关文档。
  • 由于日志的后台任务间歇性地占用硬盘的所有 I/O 带宽,写入日志可能会导致停顿(stall)。可以考虑在独立的磁盘或其他存储设备上记录日志,例如内存支持(memory-backed)的文件系统来避免这种情况。

另一种需要注意的情况是Real比其它时间的总和大得多,这说明 VM 在可能过载的机器上运行,并且没有获得足够的 CPU 时间。

对象引用处理耗时过长

关于处理对象应用耗时的信息显示在Reference Processing阶段。在Reference Processing阶段,G1 根据特定类型引用对象(Reference Object)的要求更新其引用的对象。默认情况下,G1 尝试使用如下启发式的方式将Reference Processing阶段并行化为多个子阶段:在-XX:ParallelGCThreads范围内,对于每 -XX:ReferencesPerThread 个引用对象启动一个线程。可以通过将 -XX:ReferencesPerThread 设置为 0 禁用该启发式设置,而启动所有可用线程,或者通过 -XX:-ParallelRefProcEnabled 完全禁用并行化。

Young-Only阶段中的Young GC耗时过长

一般来说,普通的Young GC的耗时大致与年轻代的大小成正比。或者更确切地说,是和CSet中需要复制的存活对象的数量成正比。如果 Evacuate Collection Set 阶段耗时过长,特别是 Object Copy 子阶段,则可以降低 -XX:G1NewSizePercent。这可以减少年轻代的最小值,允许更短的停顿。

如果应用的执行突然剧烈波动 (尤其是当大量的对象活过了一次GC) ,则可能导致年轻代调整相关的另一个问题:可能会导致垃圾收集停顿时间出现毛刺。通过降低-XX:G1MaxNewSizePercent 来减小年轻代的最大值可能会有所帮助。这会限制年轻代的最大值,因此限制了停顿期间需要处理的对象数量。

混合回收耗时过长

混合回收用于回收老年代的空间。混合回收的集合(collection set)包含年轻代和老年代Region。可以通过启用 gc+ergo+cset=trace 日志输出来获取年轻代或老年代Region对于停顿时间的占用信息。可以分别查看年轻代和老年代的 predicted young region时间和predicted old region`时间。

如果predicted young region时间太长,查看“Young-Only阶段中的Young GC耗时过长”一节中的参数配置。否则,那就要减少老年代Region对于停顿时间的占用,G1提供了三个参数:

  • 增加 -XX:G1MixedGCCountTarget 将老年代Region的回收分散到更多次的垃圾回收(译注:指的是混合回收)中。
  • 通过设置 -XX:G1MixedGCLiveThresholdPercent 避免那些需要大量时间进行回收的Region加入到候选回收Region中。在大多数情况下,高使用率的Region一般需要花费更多的回收时间。
  • 提前停止老年代的空间回收,这样 G1 就不会回收那么多高使用率的Region。在这种情况下,可以增加 -XX:G1HeapWastePercent

请注意,最后两个参数减少了当前Space-reclamation阶段中候选回收Region的数量。这可能意味着 G1 可能无法在老年代回收足够的空间来持续运行(for sustained operation)。但是后续的space-reclamation阶段可能能够对它们进行垃圾回收。

更新RS和扫描RS时间过长

为了使 G1 能够转移某个老年代Region,G1需要跟踪跨Region间的引用关系,即从一个Region指向另一个Region的引用。指向本Region的所有引用集合称为该Region的记忆集(remembered set)。当移动Region的对象(Content)时必须更新维护记忆集,这个操作大多是并发的。出于性能考虑,当应用在跨Region的两个对象之间设置引用关系时,G1 并不会立即更新记忆集。这项操作会被延迟和合批处理以提高效率。

G1 需要完整的记忆集才能进行垃圾回收,因此Update RS阶段会处理未完成的记忆集更新请求。Scan RS阶段搜索记忆集的对象引用,移动Region对象(Content),然后将这些对象的引用更新到新的位置。根据应用的不同,这两个阶段可能需要大量时间。

使用选项 -XX:G1HeapRegionSize 调整堆Region的大小会影响跨Region引用的数量以及记忆集的大小。处理Region的记忆集是垃圾回收的重要部分,因此这对最大期望停顿时间的实现有直接的影响。较大的Region往往具有较少的跨Region引用,因此处理它们所花费的相对工作量会减少。与之相反的是,较大的Region可能意味着每个Region有更多的存活对象需要转移,从而增加了其他阶段的时间。

G1 会尝试调度记忆集更新的并发处理,使得Update RS阶段的耗时和最大期望停顿时间的占比大致满足: -XX:G1RSetUpdatingPauseTimePercent 。可以减小这个值,这样G1 通常会更加并发地执行记忆集的更新工作。
当应用程序分配较大对象(large objects)时,会产生虚假的Update RS高耗时,这往往是由于如下优化工作导致的:试图将Update RS合批处理以减少并发的操作。如果应用程序的合批处理发生在垃圾回收之前,那么G1要在停顿时间内处理完所有的Update RS操作。可以使用-XX:-ReduceInitialCardMarks禁用该行为以及该行为发生的可能性。

为了让RS的大小保持在一个较低的水平,G1需要执行压缩操作,这个压缩量也决定了扫描RS的耗时。RS在内存中被压缩得越紧凑,那就需要花费越多的时间来检索内存中的值。G1 会自动执行这种被称为记忆集粗化(remembered set coarsening)的压缩操作,并根据Region当前记忆集的大小进行更新。尤其是在最高压缩级别下,检索实际数据可能非常缓慢。通过参数:-XX:G1SummarizeRSetStatsPeriod并结合gc+remset=trace级别的日志可以查看这种记忆集粗化行为是否产生。如果答案为是,参数:-XX:G1RSetRegionEntries可以显著降低这种行为。要避免在生产环境中开启这种详细的记忆集日志,因为收集这些数据可能需要大量时间。

吞吐量调优

G1 的默认策略是试图在吞吐量和延迟之间保持平衡;但是在某些情况下,我们也可能需要更高的吞吐量。除了如前几节所述减少整体期望停顿时间外,也可以通过减少停顿的频率达成这项目标。其实主要思想就是通过 -XX:MaxGCPauseMillis 来增加最大期望停顿时间。启发式的分代大小调整机制会自动调整年轻代的大小,这直接决定了垃圾回收停顿的频率。如果这没有达到预期吞吐量的目标,特别是在Space Reclamation阶段,通过-XX:G1NewSizePercent 增加最小年轻代大小将迫使 G1 增大吞吐量。

在某些情况下,-XX:G1MaxNewSizePercent (最大年轻代大小) 会限制年轻代容量的上限,进而限制了G1的吞吐量。可以通过查看gc+heap=info的Region输出进行诊断。这种情况下,Eden 区和 Survivor 区的百分比之和( combined percentage)接近所有Region总数的百分之 -XX:G1MaxNewSizePercent。此时就可以考虑增加-XX:G1MaxNewSizePercent

增加吞吐量的另一个选择是尝试减少并发工作量,特别是并发记忆集更新通常需要大量 CPU 资源。增加-XX:G1RSetUpdatingPauseTimePercent可以将这些耗时操作从并发操作期间往GC停顿期间转移。在最激进的(worst )情况下,可以通过-XX:-G1UseAdaptiveConcRefinement-XX:G1ConcRefinementGreenZone=2G-XX:G1ConcRefinementThreads=0禁用记忆集更新的并发操作。这些操作除了禁用该并发机制,还会将所有的集合更新工作移至下一次垃圾收集暂停中。

通过 -XX:+UseLargePages 启用大页面也可以提高吞吐量。有关如何设置大页面的信息,请参阅操作系统文档。

可以将参数-Xms-Xmx设置为相同的值来禁用堆大小动态调整。您可以使用 -XX:+AlwaysPreTouch 将操作系统分配物理内存到虚拟内存的操作移动到 VM 启动时。为了使暂停时间更加一致(consistent),这两项措施都是特别可取的。

堆大小调优

与其他收集器一样,G1 旨在管理(size)堆内存,使得垃圾回收所停顿的时间低于 -XX:GCTimeRatio 选项指定的比率。调整此选项以使 G1 满足您的要求。

可调整的默认值

本节介绍参数默认值及附加信息。

Table 10-1 Tunable Defaults G1 GC
参数及默认值 描述
-XX:+G1UseAdaptiveConcRefinement

-XX:G1ConcRefinementGreenZone=<ergo>

-XX:G1ConcRefinementYellowZone=<ergo>

-XX:G1ConcRefinementRedZone=<ergo>

-XX:G1ConcRefinementThreads=<ergo>

记忆集并发更新(refinement)使用这些参数控制并发refinement线程的工作分配。 G1 会为这些参数进行自动推断,以使得在垃圾回收暂停中花费 -XX:G1RSetUpdatingPauseTimePercent 时间处理剩余的工作,并根据需要自适应调整。请谨慎更改,因为这可能会导致长时间的停顿。
-XX:+ReduceInitialCardMarks 将初始对象分配的记忆集并发更新(refinement)进行合批处理。
-XX:ReferencesPerThread=1000

-XX:+ParallelRefProcEnabled

-XX:ReferencesPerThread 决定并行化程度:每N个对象引用分配1个线程,参与到Reference Processing的子阶段(线程数受-XX:ParallelGCThreads限制)。设置为 0 表示将始终使用 -XX:ParallelGCThreads 所指示的最大线程数。
-XX:+ParallelRefProcEnabled决定了 java.lang.Ref.* 实例的处理是否应该由多个线程并行完成。
-XX:G1RSetUpdatingPauseTimePercent=10 决定了G1在更新 RS 阶段的耗时占用总垃圾收集耗时的百分比。 G1 使用该值控制并发记忆集更新的数量。
-XX:G1SummarizeRSetStatsPeriod=0 这表示每隔多少个回收周期G1生成记忆集摘要报告。将此设置为0表示禁用。生成记忆集摘要报告是一项代价高昂的操作,因此仅应在必要时使用并且具有相当高的价值时,使用 gc+remset=trace 进行输出。
-XX:GCTimeRatio=12 该值代表消耗在垃圾回收上的目标时间比率公式的除数。该时间比率公式为: 1 / (1 + GCTimeRatio)。此默认值表示大约有 8% 的时间用于垃圾收集。

注意: <ergo>表示实际值根据自动推断设定。