JVM系列(十五) 垃圾收集器之 G1 MixedGC混合收集及FullGC

1,150 阅读8分钟

1.G1 MixedGC 混合收集

老年代的堆空间使用不停增加,内存占用达到了参数-XX:InitiatingHeapOccupancyPercentt(默认45%)设定的值就会触发Mixed GC,当老年代大小占整个堆大小百分比达到该阈值时,回收所有的新生代和部分老年代,超过这个值就开始触发全局标记,进而触发MixedGC

  • 根据用户设置的GC停顿时间来确定老年代垃圾收集的先后顺序。
  • G1的垃圾收集 Mixed GC,主要使用复制算法,需要把各个Region中存活的对象复制到另一个空闲的Region
  • 如果在复制过程中发现没有足够的空Region放复制的对象,那么就会触发一次Full GC。

G1 Mixed GC:
Mixed GC步骤主要分为两步:

  1. 全局并发标记(global concurrent marking)
  2. 拷贝存活对象(evacuation)

这里需要特别注意的是 Mixed GC 并不是 Full GC,只有当 Mixed GC 来不及回收old region 老年代的时候,也就说在需要分配老年代的对象时,但发现没有足够的空间,这个时候就会触发一次 Full GC

2.G1 MixedGC 全局标记阶段过程

2.1 全局并发标记(global concurrent marking)

-XX:InitiatingHeapOccupancyPercent: 默认值45,意思是老年区已使用空间/整个堆空间的比例, 如果超过阈值, 就会先触发全局标记, 进行 global concurrent marking 标记周期,当达到IHOP阈值时,G1并不会立即发起并发标记周期,而是等待下一次年轻代收集,利用年轻代收集的STW时间段,完成初始标记,这种方式称为借道(Piggybacking)。 在 G1 GC 中,它并不是一次GC过程的必须环节,通常是多次执行了YoungGC之后才会进行一次GlobalConcurrentMarking, 它主要是为 Mixed GC 提供标记服务的。 global concurrent marking的执行大致上可以划分为五个步骤:

  • 初始标记(initial mark,STW)

整个过程STW,标记了从GC Root可达的对象,这些对象全都是 GC Roots 节点以及直接可达的对象,虽然会发生stop the world,但是该阶段耗时很短,很快就会结束, 不会占用太多的停顿时间

  • 根区域扫描(root region scan)

初始标记暂停结束后,年轻代收集也完成的对象复制到Survivor存活区的工作,应用线程开始活跃起来,所有被就复制到survivor去与的对象,都需要被扫描标记为根对象,这个过程被称为根区域扫描

根区域扫描,就是标记存活区中(即 survivor 区)中指向老年代的被初始标记标记的引用的对象。它会标记所有的从所谓的根区可以到达的对象, 这个阶段与应用程序并发运行,并且只有完成该阶段后,才能开始下一次 STW 的 young GC。

  • 并发标记(Concurrent Marking)

从名字上来看, 该过程就是和应用程序并发执行,线程数量由参数 -XX:ConcGCThreads (默认GC线程数的1/4,即-XX:ParallelGCThreads/4)控制, 从 GC Roots 对堆中的对象进行可达性分析,标记整个堆Heap的存活对象,每个线程只扫描一个分区,收集每个Region的存活对象信息,标记出来存活的对象图,同时并发标记线程还会定期检查并且同时处理STAB全局缓冲区列表的记录,更新对象引用信息

并发标记的过程可能被 young GC 中断,并发标记阶段产生的新的引用(或引用的更新)会被 SATB 记录下来, 所有的标记任务必须在堆满前就完成扫描,如果并发标记耗时很长,那么有可能在并发标记过程中,又会经历多次年轻代收集。如果堆满前没有完成标记任务,则会触发担保机制,经历一次长时间的串行Full GC

在并发标记过程中,会计算每个区域中对象的存活比例。如果在此阶段中,如果发现区域中的所有对象都是垃圾,那这个区域会立即被回收。

  • 重新标记(Remark,STW)

重新标记也叫最终标记 final marking 阶段,为了修正在并发标记期间,因应用程序继续运作而导致标记产生变动的那一部分标记记录,G1需要一个暂停的时间,去处理剩下的SATB日志缓冲区和所有更新

G1 使用的是比 CMS 更快的初始快照算法 SATB 算法:snapshot-at-the-beginning。它只需要扫描SATB(Snapshot At The Beginning)的buffer,处理在并发阶段产生的新的存活对象,去处理剩下的 SATB日志缓冲区和所有更新,找出所有未被访问的存活对象

对比CMS 垃圾收集器,CMS的 remark重新标记阶段需要扫描整个mod union table的标记为dirtyentry以及全部根

  • 清除(Cleanup, STW)

该阶段STW, 该阶段会计算每一个Region里面存活的对象,并把完全没有存活对象的Region直接放到空闲列表中。在该阶段还会重置Remember Set, 并且对每个Region进行排序

排序是为了找出各个 Region 的回收价值和成本,并根据用户所期望的GC停顿时间来制定回收计划。(这个阶段并不会实际去做垃圾的回收,也不会执行存活对象的拷贝)

清除阶段执行的详细操作如下:

  1. RSet梳理:启发式算法会根据活跃度和RSet尺寸对每一个Region分区定义不同等级,同时RSet梳理也有助于发现无用的引用。
  2. 整理堆分区:目的是区分混合收集识别回收收益高(基于释放空间和暂停目标)的老年代分区集合;
  3. 识别所有空闲分区:即发现没有任何对象存活对象的分区,该分区可在清除阶段直接回收,无需等待下次收集周期。
2.2 并发标记后
  • 并发标记结束以后,老年代中100%为垃圾的 region 就直接被回收了,仅部分为垃圾的region会进行混合回收
  • 根据停顿目标,G1 可能没法一次性回收掉所有的old region 候选分区,只能选择优先级高的若干个 region 进行回收
  • 对优先级高的,回收垃圾性价比高的Region 被分成8次回收(可以通过 -XX:G1MixedGCCountTarget 设置,默认阈值8)
  • 垃圾占内存分段比例越高的,越会被先回收。由参数-XX:G1MixedGCLiveThresholdPercent(默认65%)阈值决定内存分段是否被回收
  • 如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间,计算出来回收性价比不高
  • 混合回收并不一定要进行8次,由参数-XX:G1HeapWastePercent(默认值 10%)控制
  • -XX:G1HeapWastePercent允许整个堆内存有 10% 的空间浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收,这样利用率比较高
  • 否则 GC计算完,回收该区域会花费很多的时间,但是回收到的内存却很少,回收性价比不高
2.3 拷贝存活对象(evacuation)

对象复制过程又叫作Evacuation,是一个STW过程。

  • 这一步会复用YongGC的逻辑,只是正常YGC的Choose Set只选择Young Region分区
  • Mixed GC复用YongGC代码,在创建Choose Set时会选择所有Young Region和部分收益较高的Old Region
  • 将CSet中的存活对象复制到Survivor Region存活区,然后回收原来的Region空间
  • 如果发现没有足够的Region能够存放这部分拷贝的对象,就会触发FullGC

3. Full GC

FullGC采用的是 Serial Old GC, 单线程收集老年代对象

  • 停止系统程序,单线程进行标记、清理和压缩整理
  • Full GC 会对整堆做标记清除和压缩, 默认参数 XX:G1ReservePercent(默认10%)可以保留空间 用于垃圾回收
  • 清理空间,腾出空闲的一批Region来供下一次Mixed GC使用
  • 该过程是非常耗时的,而且Full GC的收集代价非常高, 能够明显感觉到应用程序的停顿
  • 应该尽量避免Full GC的发生。

G1在以下场景中会触发 Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure:

  1. 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
  2. 从老年代分区转移存活对象时,无法找到可用的空闲分区
  3. 分配巨型对象时在老年代无法找到足够的连续分区,也会提前触发FullGC

至此, 我们讲解了G1 垃圾收集器的Mixed GC的 垃圾回收过程, 明天我们分析 下G1的 垃圾回收日志