【G1】性能调优 - shenjy24/jackal-gc GitHub Wiki
一. G1 推荐配置
一般建议是使用 G1 的默认设置,可以使用-XX:MaxGCPauseMillis 指定一个暂停时间目标,并在需要时使用 -Xmx 设置最大  Java 堆大小。
G1 默认值的平衡方式与其他收集器不同。 G1 在默认配置中的目标既不是最大吞吐量也不是最低延迟,而是在高吞吐量下提供相对较小、均匀的暂停。 然而,G1 增量回收堆空间和暂停时间控制的机制在应用程序线程和空间回收效率方面都会产生一些开销。
如果您更倾向于高吞吐量,可以使用 -XX:MaxGCPauseMillis 放宽暂停时间目标或提供更大的堆容量。如果延迟是主要要求,则可以适当调小暂停时间。避免使用-Xmn 、-XX:NewRatio之类的选项来将年轻代限制为特定值,因为年轻代大小是 G1 允许其满足暂停时间的主要手段,将年轻代大小设置为特定值会覆盖并实际上禁用暂停时间控制。
二. 从其他垃圾收集器迁移到 G1
通常,当从其他收集器(尤其是 CMS 收集器)迁移到 G1 时,首先删除所有影响垃圾收集的参数,仅需设置暂停时间目标,以及使用 -Xmx 和可选的 -Xms 设置堆大小。
许多对其他收集器有用的参数,要么根本没有效果,要么甚至会降低吞吐量和达成暂停时间目标的可能性。一个例子是设置年轻代大小,这会完全阻止 G1 调整年轻代大小以满足暂停时间目标。
三. 提升 G1 性能
G1 旨在提供良好的整体性能,而无需指定额外的配置参数,但是在某些情况下,G1 的默认启发式或默认配置只提供了次优结果。根据具体情况,应用程序级优化可能比尝试调整 VM 行为更有效,例如减少生命周期长的对象数量。
出于诊断目的,G1 提供了全面的日志记录。 一个好的开始是使用 -Xlog:gc*=debug 参数,然后在必要时优化输出。该日志提供了关于垃圾收集活动暂停期间和暂停之外的详细概述,这包括收集的类型和在暂停的特定阶段花费的时间信息。
下面的小节将探讨一些常见的性能问题。
1. 观测 Full GC
Full GC 通常会花费较长时间。老年代占用过高导致的 Full GC 可以通过在日志中找到 Pause Full (Allocation Failure) 字样来检测到。Full GC 之前通常会遇到由 to-space exhausted  标记的迁移(evacuation)失败的垃圾收集。
发生 Full GC 的原因是应用程序分配了太多无法足够快地被回收的对象, 导致并发标记无法及时完成以启动空间回收阶段。应用分配许多大对象也会提升 Full GC 发生的可能性,由于大对象在 G1 中的分配方式,它们可能会占用比预期更多的内存(空余的Region空间无法再用)。
G1 为您提供了几个选项来更好地处理这种情况:
- 您可以使用 
gc+heap=info日志记录确定堆上大对象占用的 region 数。Humongous regions: X->Y行中的 Y 为您提供大对象占据的.region 数,如果这个数字与老年代的 region 数量相比很高,最好的选择是尝试减少这个对象的数量。 您可以通过使用-XX:G1HeapRegionSize选项增加 region 大小来实现这一点,当前设置的 region 大小打印在日志的开头。 - 增加 Java 堆的大小。 这通常会增加标记必须完成的时间量。
 - 通过 
-XX:ConcGCThreads参数增加并发标记线程的数量。 - 强制 G1 提前开始标记。G1 根据早期的应用程序行为自动确定初始堆占用百分比 (IHOP) 阈值。如果应用程序行为改变,则这些预测可能就是错误的。有两个做法来强制提前标记: 通过修改 
-XX:G1ReservePercent来增加自适应 IHOP 计算中使用的缓冲区,从而降低开始空间回收的目标占用率。或者通过使用-XX:-G1UseAdaptiveIHOP和-XX:InitiatingHeapOccupancyPercent手动设置来禁用 IHOP 的自适应计算。 
除了Allocation Failure导致的Full GC 之外,其他原因通常是应用程序或某些外部工具导致了整堆收集。如果原因是 System.gc(),并且没有办法修改应用程序,可以通过使用 -XX:+ExplicitGCInvokesConcurrent 来减轻 Full GC 的影响,或者通过设置 -XX:+DisableExplicitGC 让虚拟机完全忽略掉显示 GC 调用。
2. 大对象导致碎片化
Full GC可能会在所有 Java 堆内存耗尽之前发生,因为需要为大对象找到一组连续的 region。在此种情况下,潜在的选项是使用-XX:G1HeapRegionSize来增加 region 的大小或者增加堆容量,以减少大对象的数量。在极端的情况下,可能没有足够的连续空间供 G1 分配对象,即使是有足够的可用内存。如果Full GC不能回收足够的连续空间,就会导致 VM 退出。因此,除了前面提到的减少大对象分配量或增加堆之外,没有其他选择。
3. 延迟调优
本节讨论在常见延迟问题(即暂停时间太长)的情况下改进 G1 行为的建议。
(1) Unusual System or Real-Time Usage
对于每一次垃圾收集暂停,gc+cpu=info 日志输出都包含一行来自操作系统的信息,其中包含暂停时间所花费的详细信息。示例如: User=0.19s Sys=0.00s Real=0.01s。
User 是花费在虚拟机代码上的时间,Sys 是花费在操作系统上的时间,Real 是在暂停过程中经过的绝对时间。
常见的高系统时间问题有:
- 虚拟机从操作系统内存中分配或归还内存可能会导致不必要的延迟。通过使用
-Xms和-Xmx将最小和最大堆大小设置为相同的值,并使用-XX:+AlwaysPreTouch预分配所有内存,以将这项工作移到VM启动阶段,从而避免延迟。 - 特别是在 Linux 中,通过
Transparent Huge Pages(THP) 功能将小页面合并为大页面往往会停止随机进程,而不仅仅是在暂停期间。因为 VM 分配和维护大量内存,所以 VM 成为长时间停滞的进程的风险比通常更高。关于如何禁用透明大页特性,请参考操作系统的文档。 - 日志输出可能会出现一段时间的停顿,原因是某个后台任务间歇占用了写入日志的硬盘的所有I/O带宽。考虑为您的日志或其他一些存储使用单独的磁盘,例如内存支持的文件系统,以避免这种情况。
 
需要注意的另一种情况是,Real 时间比其他情况的总和大得多,这可能表明 VM 在可能过载的机器上没有获得足够的CPU时间。
(2) 引用对象处理时间过长
处理引用对象所需时间的信息显示在引用处理阶段。在引用处理阶段,G1根据特定类型的引用对象的要求更新引用对象的引用。默认情况下,G1尝试使用以下启发式方法并行化引用处理的子阶段:对于每 -XX:ReferencesPerThread 个引用对象启动一个线程,并发线程数由 -XX:ParallelGCThreads 中的值限制。 可以通过将 -XX:ReferencesPerThread 设置为 0 来禁用此启发式,以默认使用所有可用线程,或者通过 -XX:-ParallelRefProcEnabled 完全禁用并行化。
(3) 年轻代收集耗时过长
一般来说,年轻代收集花费的时间大致与年轻代的大小成正比,或者更具体地说,与需要复制的存活对象的数量成正比。 如果Evacuate Collection Set阶段花费的时间过长,特别是Object Copy子阶段,则降低-XX:G1NewSizePercent参数的值。 这减少了年轻代的最小值,允许可能更短的停顿。
如果应用程序性能或者幸存对象数量突然发生变化,则可能会出现另一个与年轻代大小有关的问题, 这可能会导致垃圾收集暂停时间出现峰值。降低-XX:G1MaxNewSizePercent参数的值,对此种情况可能有所帮助。这会限制年轻代的最大大小,从而限制暂停期间需要处理的对象数量。
(4) 混合收集耗时过长
混合收集常用来回收老年代的空间 ,其CSet包含新生代和老年代的 Region。通过启用gc+ergo+cset=trace 日志输出,可以获得新生代和老年代 Region 的时间回收对暂停时间的详细信息。
通过观测新生代和老年代 Region 回收的预测时间可以采取以下做法优化延迟:
如果是新生代 Region 回收耗时过长,可以参考上面的年轻代收集耗时过长小节。
如果是老年代 Region 回收耗时过长,G1提供了三个参数来进行优化:
- 
调高
-XX:G1MixedGCCountTarget参数的值,这次参数指定在一次回收过程中做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。 - 
调低
-XX:G1MixedGCLiveThresholdPercent参数的值,该参数的默认值是85%。规定只有存活对象低于85%的 Region 才可以被回收。G1是基于复制算法来处理垃圾对象的,把存活对象复制到另一个空闲的Region,剩下的就是垃圾对象了,然后就可以直接回收整个Region。如果一个Region中的存活对象大于85%,把这85%对象都复制到另一个空闲的Region,成本还是很高的,而且Region改变也不大,所以回收价值不高,因此可以让G1把有限的时间拿去回收那些回收价值更大的Region,以此把GC停顿时间控制在范围内。 - 
调高
-XX:G1HeapWastePercent参数的值,这样尽早停止老年代空间回收,这样 G1 就不会收集那么多高占用区域。 
请注意,最后两个选项减少了当前空间回收阶段CSet候选区域的数量。 这可能意味着 G1 无法在老年代回收足够的空间来持续运行。 但是,稍后的空间回收阶段可能能够对它们进行垃圾收集。
(5) 更新 RS 和扫描 RS 次数过高
为了使G1能够迁移单个老年代区域,G1跟踪跨区域引用的位置,即从一个区域到另一个区域的引用,指向给定区域的跨区域引用集合称为该区域的记忆集。当移动一个区域的内容时,记忆集必须更新,区域记忆集的维护大多是并发的。出于性能考虑,当应用程序在两个对象之间有新的跨区域引用时,G1不会立即更新记忆集,为了提高效率,记忆集的更新请求会被延迟以进行批处理。
G1需要完整的记忆集来进行垃圾收集,因此垃圾收集的 Update RS 阶段处理任何未完成的记忆集更新请求。Scan RS 阶段在记忆集中搜索对象引用,移动区域内容,然后将这些对象引用更新到新的位置。根据应用程序的不同,这两个阶段可能会花费大量的时间。
通过使用参数 -XX:G1HeapRegionSize 调整区域的大小会影响跨区域引用的数量以及记忆集的大小。处理区域的记忆集是垃圾收集工作的重要部分,因此这对可实现的最大暂停时间有直接影响。较大的区域往往具有较少的跨区域引用,因此处理它们所花费的相对工作量会减少,但是与此同时,较大的区域可能意味着每个区域需要迁移更多的存活对象,从而增加了其他阶段的时间。
G1 尝试调度记忆集更新的并发处理,以便Update RS 阶段大约占用允许的最大暂停时间的 -XX:G1RSetUpdatingPauseTimePercent 参数指定的百分比。 通过减小这个值,G1 通常会同时执行更多的记忆集更新工作。
与应用程序分配大对象相关联的高Update RS时间的假象,可能是由试图通过批处理来减少记忆集的并发更新工作的优化引起的。如果创建此类批处理的应用程序恰好发生在垃圾收集之前,那么垃圾收集必须在暂停的 Update RS 时间部分处理所有这些工作。可以使用-XX:-ReduceInitialCardMarks禁用此行为并可能避免这些情况。
扫描记忆集时间也由 G1 为保持记忆集的低存储容量而执行的压缩量有关。记忆集存储在内存中越紧凑,在垃圾收集期间检索存储的值所花费的时间就越多。G1自动执行这种压缩,称为记忆集粗化(remembered set coarsening),同时根据该区域记忆集的当前大小更新记忆集。特别是在最高压缩级别时,检索实际数据可能非常慢。选项 -XX:G1SummarizeRSetStatsPeriod 结合 gc+remset=trace 级别日志显示是否发生粗化,如果是,那么在 日志中 Before GC Summary 部分的Did <X> coarsenings行中的 X 会显示了一个高值。可以显著增加-XX:G1RSetRegionEntries参数的值,以减少这些粗化的数量。避免在生产环境中使用这种详细的记忆集日志记录,因为收集这些数据可能会花费大量时间。
4. 吞吐量调优
G1 的默认策略试图在吞吐量和延迟之间保持平衡,但是在某些情况下需要更高的吞吐量。除了如前几节所述的可以减少整体暂停时间外,还可以降低暂停的频率,核心思想是通过使用 -XX:MaxGCPauseMillis 来增加最大暂停时间,G1 会自适应地调整年轻代的大小,这将会直接影响暂停的频率。如果这不会导致预期的行为,特别是在空间回收阶段,使用-XX:G1NewSizePercent增加年轻代的最小容量将迫使G1这样做。
在某些情况下,-XX:G1MaxNewSizePercent 参数设定的最大年轻代大小,可能会通过限制年轻代大小来限制吞吐量。这可以通过查看 gc+heap=info 日志的 Region 汇总输出信息来诊断。如果 Eden 区域和 Survivor 区域的总和百分比接近 -XX:G1MaxNewSizePercent 参数设定的百分比,则可以考虑增加 -xx:G1MaxNewSizePercent的值。
提高吞吐量的另一个选择是尝试减少并发工作量——特别是,记忆集的并发更新通常需要大量的CPU资源。增加 -XX:G1RSetUpdatingPauseTimePercent的值会将工作从并发操作转移到垃圾收集暂停。在最坏的情况下,可以通过设置-XX:-G1UseAdaptiveConcRefinement -XX:G1ConcRefinementGreenZone=2G -XX:G1ConcRefinementThreads=0 来禁用并发记忆集更新。这通常会禁用该机制,并将所有记忆集更新工作移到下一次垃圾收集暂停时。
通过使用-XX:+UseLargePages来启用大页面也可以提高吞吐量。关于如何设置大页面,请参考操作系统文档。
您可以通过禁用自动调整堆大小来最小化堆大小调整工作,即 将选项 -Xms 和 -Xmx 设置为相同的值。 此外,您可以使用 -XX:+AlwaysPreTouch 将操作系统有关虚拟内存与物理内存的工作移到 VM 启动时间。 为了使暂停时间更加一致,这两种措施都是特别可取的。
5. 堆容量调优
与其他收集器一样,G1 旨在调整堆大小,以使垃圾收集所花费的时间低于 -XX:GCTimeRatio 参数确定的比率。可以调整此参数以使 G1 满足您的要求。
-XX:GCTimeRatio 表示希望在GC花费不超过应用程序执行时间的1/(1+nnn),nnn为大于0小于100的整数。举个官方的例子,参数设置为19,那么GC最大花费时间的比率=1/(1+19)=5%,程序每运行100分钟,允许GC停顿共5分钟,其吞吐量=1-GC最大花费时间比率=95%。默认情况下,G1设置此值为12,运行用户代码时间大约是GC停顿时间的12倍,即GC最大花费时间比率约为8%。