G1垃圾回收优化

G1垃圾回收优化

原文地址:https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector-tuning.htm#JSGCT-GUID-90E30ACA-8040-432E-B3A0-1E0440AB556A
Topics
● G1通用推荐
● 从其他收集器转换到G1
● 提高G1性能
○ 观察Full Gc
○ 大对象碎片
○ 优化延迟
   ■ 特殊系统或者实时要求
   ■ 处理对象引用过慢
   ■ 新生代回收速度过慢
   ■ 回合回收过程过慢
   ■ 过长的 Update RS 和Scan RS时间
○ 吞吐量优化
○ 堆内存大小优化
○ 默认优化

G1通用设置

       一般情况下我们推荐使用默认的G1设置,我们仅仅需要给出一个自己想要的目标暂停时间和最大堆内存值既可。与其他的垃圾回收机制不同,G1默认设置的目标既不是最大吞吐值也不是最低延迟,而是在兼顾二者的情况下保持平衡。与此同时,G1的分步回收机制和gc暂停时间控制也会给我们的应用和垃圾回收带来一些额外的开销。
       如果你希望吞吐量尽可能的高,可以提高堆内存大小或者提高可暂停时间大小。如果低延迟是主要的需求,那就可以降低目标暂停时间。同时避免对新生代内存大小进行太大限制,因为新生代的内存大小是G1用来控制暂停时间的主要因素。但是如果设置了指定的新生代大小将会导致暂停时间控制的机制被禁用。

从其他收集器转移到G1

       通常来说,如果从其他收集器转移到G1,尤其是从CMS,我们需要删除所有的jvm参数,然后只需要加上目标暂停时间和堆内存大小这两个参数即可。

提高G1的性能

       G1的设计目的就是用尽可能少的配置来达到综合相对高的性能。但是尽管如此,依然有很多情况下默认设置并不能达到最合适的性能。这部分内容会告诉我们如何检测和提高性能,当然这些指导并不保证100%会提高性能,具体情况还需要具体对待。为了便于我们检测问题,G1提供了广泛的日志输出。如果需要的话我们可以使用-Xlog:gc*=debug来优化GC的输出。打开日志后我们能看到暂停期间更详细的日志。这包括了各种垃圾收集类型和各个收集阶段使用到的时间,接下来的部分我们会探索一些常见的性能问题:

Full GC

       Full GC通常都非常耗时,当老年代的堆内存占有量非常高的时候会导致分配空间失败从而触发一次Full GC。Full GC通常都发生在疏散对象失败(to-space exhausted)之后。
       Full GC发生的原因一般都是应用分配了太多对象而无法回收导致。这种情况下,并发标记无法及时完成且无法开始回收空间。其中有一种可能性是连续分配了过多的大对象,根据G1中大对象的分配策略,他们可能占据了远超我们想象的空间。
       所以我们的目标是让并发标记能够及时完成,我们可以通过减少老年代的分配又或者是通过提高允许并发标记占据时间来达成:
G1 提供了几种好的选项:
       ● 当java堆中存在大量的大对象, 那么 gc+heap=info日志会展示大对象区块的下一个区块号。在每次垃圾回收之后,最好的选项是去尝试减少大对象数量。你可以通过-XX:G1HeapRegionSize来提高区块大小来达到这个目的,因为只有达到区块一半大小的对象才被视为大对象。当前选用的区块大小会在gc日志头部打印出来
       ● 提高java堆内存大小,这将显著提高标记可以占用的时间
       ● 提高并发标记线程数目,设置-XX:ConcGCThreads .
       ● 强制G1提前进行标记,G1通过IHOP参数和之前收集的应用行为来决定何时进行标记,如果应用行为发生了变化,这些预测就不再准确。这里我们有两个选项:通过修改-XX:G1ReservePercent来降低目标占用值的阈值;或者禁用IHOP自适应计算,这个可以通过 -XX:-G1UseAdaptiveIHOP 和-XX:InitiatingHeapOccupancyPercent来设置。

       其他Full GC可能的原因可能是外部工具或者是调用System.gc()导致,如果我们无法修改代码的话,我们可以通过-XX:+ExplicitGCInvokesConcurrent参数减缓这些行为,甚至可以直接禁用这类GC,具体的参数是XX:+DisableExplicitGC。至于外部的一些工具,例如jmap这些就只有外部可以控制了。

大对象碎片

       即便jvm内存没有用光也有可能会因为无法分配连续的区域二导致Full GC。这里可以调整区块大小来减少大对象数量,或者提高堆的大小。极端情况下即使Full GC之后仍然没法找到足够的空间,这会导致jvm异常退出。

优化延迟

特殊系统或者实时系统
       对于每次gc停顿,gc+cpu=info日志都会包含一行关于停顿所发生的信息。比如像这样: User=0.19s Sys=0.00s Real=0.01s.
       User时间是指vm部分代码花掉的时间,system时间是操作系统消耗的时间,real 时间是指暂停用掉的绝对时间。如果system时间相对较高,那一般都是由系统环境因素造成。

常见的system时间过高的原因:
       ● VM在分配和返还内存给os的时候可能会造成不必要的延迟。可以通过将最大-Xmx和最小堆内存 -Xms大小设置成一样的值来避免这个操作,并且可以使用-XX:+AlwaysPreTouch来预分配内存,从而将这个步骤移动到VM启动的阶段。
       ● 尤其在linux系统中,Transparent Huge Pages (THP) 特性将小的内存页面文件合并到大的页面文件中一般都是一个随机的过程,而不仅仅在gc暂停期间进行。因为VM分配和维持了很多的内存,所以VM很有可能成为THP过程中的瓶颈从而导致很大的系统时延。请用户检查自己系统对应的使用手册来关闭THP特性以改善此问题
        ● 日志的输出也可能会成为gc问题的因素之一,因为很可能有一些后台进程占据了全部的I/O带宽从而导致日志的输出受到影响。可以考虑使用一个独立的磁盘来输出我们的日志,甚至可以考虑使用基于内存的存储来存储日志。

处理对象引用时间过长
       处理对象引用使用的时间会在Ref Proc和Ref Enq阶段展示出来。在Ref Proc阶段,G1通过引用对象类型来更新对应对象的引用,如果他们引用的对象已经死亡,G1将引用对象入到一个相对引用队列中。如果这个过程耗时太长,可以通过打开-XX:+ParallelRefProcEnabled选项来启用并行处理特性。

年轻代回收过慢
       年轻代,通常来说任何年轻代都会按照比例去分割。如果疏散回收阶段耗时过长,可以通过降低-XX:G1NewSizePercent来进行改善。这会降低年轻代的最小大小,从而带来更短的暂停时间。
       另一个问题是当应用中存活的对象数量突然发生变化的时候,会对gc的暂停时间有很大影响。我们可以通过-XX:G1MaxNewSizePercent参数降低最大年轻代的大小,从而减少暂停期间最大需要回收的年轻代对象。

混合回收过慢
       混合回收是用来回收老年代空间用的。混合回收涉及的区域包括了年轻代和老年代,可以通过打开gc+ergo+cset=trace 来获取到老年代和年轻代分别在暂停时间中所占比例的信息。分辨观察预测的年轻代和老年代时间,如果预测的年轻代时间过长,可以参阅年轻代回收过慢章节,如果是老年代造成的问题,那么G1提供了3个选项:

       ● 通过将老年代的回收分散到多次gc中,具体可以提高-XX:G1MixedGCCountTarget参数
       ● 降低需要加入回收候选集的区块占用比例,通过XX:G1MixedGCLiveThresholdPercent进行设置,很多情况下,被大量占用的区块回收时间会很长
       ● 提前停止老年代的收集工作,这样G1就不会花太多时间在这些被大量占用的区块上,这种情况提高-XX:G1HeapWastePercent参数。

       注意后两种情况虽然减少了老年代回收时间,但是同时也会导致G1无法回收足够的空间,造成一部分垃圾没被回收。但是尽管如此,在接下来下一次的回收中这些对象依然还是会被回收掉。

Update RS 和Scan RS时间过长
       使用G1来疏散回收老年区块,G1会追踪region之间的位置引用,涉及到从一个region到另一个region的引用。一个指向指定区块的其他region的引用集合被称作区块的remembered set,当移动区块中的内容的时候remembered set也需要同时进行更新。维护区块的remembered set的行为是并发的。当应用在新增region之间引用的时候G1不会立马更新remembered set,取而代之的是进行延迟批量更新,当然这一切都是为了性能。
       G1需要完整的remembered set信息来进行垃圾回收,所以Update RS阶段会响应所有的remembered set 的更新请求。Scan RS阶段会在remembered sets中搜索对象引用,移动区块内容,然后将这些对象引用更新到新的位置。这两个阶段将根据应用的特性花费不同的时间。
       通过调整-XX:G1HeapRegionSize参数来调整区块大小,这将会影响区块间引用的数量和remembered set的大小。处理区块的remembered set是垃圾回收过程中一个非常重要的部分,所以这对能达到最大目的暂停时间有很大的影响。大一点的区块一般区块间的引用会少一些,所以相对处理他们的时间会少一些,但是大一点的区块也意味着更多存活对象和更多的疏散回收时间。
       G1会尝试对remembered set在非GC时间进行并行处理,所以Update Rs阶段会根据-XX:G1RSetUpdatingPauseTimePercent参数来决定需要使用多少时间进行操作。通过降低这个值,G1通常会尽量多得并行处理更多的remembered set。
应用分配大对象和remembered set操作同时执行的时候也许会造成Update Rs时间过大的假象。如果应用在垃圾回收之前开始了一个类似的并行工作,gc会将这部分时间纪录在update RS中。使用-XX:-ReduceInitialCardMarks 来禁用这种同时发生的行为,从而防止出现类似情况。

       Scan RS时间很大程度上受到remembered set压缩幅度的影响。内存中的remembered set压缩得越厉害对应消耗的时间就越长。G1自动进行压缩,这被称作remembered set粗化,更新当前remembered set很依赖当前区块的remembered set的压缩情况,在高度压缩的情况下,获取数据会很慢。-XX:G1SummarizeRSetStatsPeriod参数 和gc+remset=trace级别日志可以显示粗化的过程。如果这样在日志中查看Did coarsenings中X的值很高的话,我们可以提高-XX:G1RSetRegionEntries 选项来降低粗化的程度。不过记得在生产环境不要打开remembered set的详细日志,这会带来很大的开销。

吞吐量优化

       G1的默认策略会尝试在吞吐量和延迟中保持平衡,尽管如此,有些情况下我们就是需要高的吞吐量。除了按照之前的说明来减少总的gc时间,gc的频率也可以进行减少。主要的方式是提高-XX:MaxGCPauseMillis参数。G1会自适应得调整年轻代的大小,这将会直接影响暂停的频率。如果不能得到理想的结果,尤其在回收阶段,提高最小年轻代值-XX:G1NewSizePercent 来让G1强制减少暂停频率。
       在有些情况下,最大可允许的年轻代大小设置值,可能会限制吞吐量。我们可以通过观察gc+heap=info日志来检测分析。可以通过增大-XX:G1MaxNewSizePercent值来解决此类问题。
       另一个优化选项是通过降低uodate RS的并发量来获取更高的吞吐量,因为该操作往往会需要很多cpu资源。提高 -XX:G1RSetUpdatingPauseTimePercent 来将update RS 操作部分转移到gc停顿期间执行。在最差的情况下,我们可以通过-XX:-G1UseAdaptiveConcRefinement -XX:G1ConcRefinementGreenZone=2G -XX:G1ConcRefinementThreads=0选项来禁用并发update RS操作。这将把update RS操作全部移动到下一次gc停顿期间执行。
       通过 -XX:+UseLargePages 参数启用大的内存页面也可以提高吞吐量。具体操作可以参见对应的操作系统文档。

你可能感兴趣的:(java,jvm,G1)