G1的GC模式分析

原文地址:https://blog.csdn.net/feelwing1314/article/details/89412397 感谢原作者

一  YGC

  G1的YGC要做的事情分为两种情况,年轻代存活的对象会被从多个Region(Eden)中拷贝并移动到1个或多个Region(S区),这个过程就叫做Evacuation

  如果某些对象的年龄值达到了阈值,就会晋升到Old区,这一点和以前的GC类似  

   

  

  G1的YGC也是一个完全STW,且多线程并行执行的阶段,对应的日志如下所示:

[GC pause (G1 Evacuation Pause) (young) 898M->450M(1024M), 0.0005367 secs]

  并且为了下一次YGC,Eden和Survivor的大小会被重新计算,计算过程中用户设置的停顿时间目标也会被考虑进去,如果需要的话,它们的大小可能调大,也可能调小。

  最后对G1的YGC做一个简单的总结:

  • 堆是一个被划分为多个Region的单独的内存空间;
  • 年轻代的内存由多个不连续的Region组成,这样的设计在需要对年轻代大小扩容的时候,变得更容易;
  • G1的YGC是完全STW的,所有的应用线程都需要停止工作;
  • YGC是多线程并行的;
  • 存活的对象会被拷贝到新的Survivor或者Old类型的Region中;

二  并发标记周期

  全局并发标记周期,即concurrent marking cycle,G1的全局并发标记周期和CMS的并发收集过程非常相似。不过,G1模式下满足触发全局并发标记的条件由参数(-XX:InitiatingHeapOccupancyPercent=45)控制,这个比例是整个Java堆占用百分比阈值,即Java堆占用这么多空间后,就会进入初始化标记->并发标记->最终标记->清理的生命周期。

  一般在生产中该比例一般会设置为60-75,45太小了会导致频繁的标记。

XX:InitiatingHeapOccupancyPercent=percent
Sets the percentage of the heap occupancy (0 to 100) at which to start a concurrent GC cycle. It is used by garbage collectors that trigger a concurrent GC cycle based on the occupancy of the entire heap, not just one of the generations (for example, the G1 garbage collector).
By default, the initiating value is set to 45%. A value of 0 implies nonstop GC cycles. The following example shows how to set the initiating heap occupancy to 75%:

-XX:InitiatingHeapOccupancyPercent=75

  G1的GC日志样例参考如下:

 1  [GC pause (G1 Evacuation Pause) (young) (initial-mark) 857M->617M(1024M), 0.0112237 secs]
 2     [GC concurrent-root-region-scan-start]
 3     [GC concurrent-root-region-scan-end, 0.0000525 secs]
 4     [GC concurrent-mark-start]
 5     [GC concurrent-mark-end, 0.0083864 secs]
 6     [GC remark, 0.0038066 secs]
 7     [GC cleanup 680M->680M(1024M), 0.0006165 secs]
 8     [GC pause (G1 Evacuation Pause) (young) 869M->665M(1024M), 0.0084004 secs]
 9     [GC pause (G1 Evacuation Pause) (mixed) 677M->667M(1024M), 0.0136266 secs]
10     [GC pause (G1 Evacuation Pause) (mixed) 711M->675M(1024M), 0.0101436 secs]
11     [GC pause (G1 Evacuation Pause) (mixed) 715M->682M(1024M), 0.0114279 secs]
12     ... ...

  由日志可知,G1的并发标记周期主要包括如下几个过程:

  1 初始标记 (initial mark)

  2 根扫描     (Root region scanning)

  3 并发标记   (concurrent marking)

  4 最终标记  (remark)

  5 清理   (cleanup)

  初始化标记

  STW阶段,在G1中,初始化标记是伴随一次普通的YGC发生的,这么做的好处是没有额外的、单独的暂停阶段,这个阶段主要是找出所有的根Region集合。
GC日志中有(young) (initial-mark)字样。

  这个阶段只是找出根Region集合,该阶段比较消耗时间。

  根分区扫描

  并发阶段,扫描那些根分区(Region)集合–Oracle官方介绍的根分区集合是那些对老年代有引用的Survivor分区,标记所有从根集合可直接到达的对象并将它们的字段压入扫描栈(marking stack)中等到后续扫描。G1使用外部的bitmap来记录mark信息,而不使用对象头的mark word里的mark bit(JDK12的Shenandoah GC是使用对象头)。这个过程是和应用线程一起运行的,另外需要注意的是,这个阶段必须在下一次YGC发生之前完成,如果扫描过程中,Eden区耗尽,那么一定要等待根分区扫描完成还能进行YGC。注意不会STW。

  并发标记

  并发阶段,继续扫描,不断从上一个阶段的扫描栈中取出引用递归扫描整个堆里所有存活的对象图。每扫描到一个对象就会对其标记,并将其字段压入扫描栈。重复扫描过程,直到扫描栈清空。另外,需要注意的是,这个阶段可以被YGC中断。

  最终标记

  STW阶段,彻底完成堆中存活对象的标记工作,使用的是SATB(snapshot-at-the-beginning)算法,它比CMS使用的算法更快。因为,G1这个remark与CMS的remark有一个本质上的区别,那就是这个暂停只需要扫描SATB buffer,应该还有RSET。这部分该篇文章写的还不够细,我会继续找资料补充。

  G1GC 用的 Snapshot-At-The-Beginning 的 Write Barrier ,并不持续跟踪对象图的变化,而是打下 concurrent mark 那一刻的快照,而将之后所有新分配的对象统统视为活跃不管,做到:

- Anything live at Initial Marking is considered live.
- Anything allocated since Initial Marking is considered live.

SATB Barrier 不需要最后的 remark,代价就是因为新分配的对象统统视为活跃,有更多的 float garbage。最后回收到的垃圾对象,一定是开始 mark 那一刻之前产生的垃圾。

这时要跟踪的不是新引用的赋值,反而是旧引用的被解除,以维持快照时刻的对象关系:

write_barrier_slot(Object* src, Object** slot, Object* new_ref) { 
    old_ref = *slot;
    if( !is_marked(old_ref) ){
      enqueue(old_ref);
    }
    *slot = new_ref;
}

  但是 G1GC 为什么仍有最终标记阶段?G1GC 中 Write Barrier 产生的标记并不是实时更新的,而会记录在本线程的 update buffer 中(它扮演的角色有点类似 golang 里的 chan?),当写满一个 buffer 后,再把整个 buffer 加入到全局的 update buffer 队列中,供 Refinement Thread 消费来真正地做 Mark。到最终标记阶段,需要做的事情就是把这些 buffer 都给 flush 出来,完成所有标记,这点与 CMS 的 Remark 有很大不同。

 

  清理阶段

  STW阶段。这个过程主要是从bitmap里统计每个Region被标记为存活的对象,计算存活对象的统计信息,然后将它们按照存活状况(liveness)进行排列。并且会找出完全空闲的Region,然后回收掉这些完全空闲的Region,并将空间返回到可分配Region集合中。需要说明的是,这个阶段不会有拷贝动作,因为不需要,清理阶段只回收完全空闲的Region而已,还有存活对象的Region,需要接下来的Mixed GC才能回收。

上面提到的这几个过程就是并发标记周期的全过程:初始化标记(initial mark)、根分区扫描(Root region scanning)、并发标记(concurrent marking)、最终标记(remark)、清理阶段(cleanup)。

    Evacuation

  STW阶段,这个阶段会把存活的对象拷贝到全新的还未使用的Region中,G1的这个过程有两种选定CSet的模式。既可能由YGC来完成,只回收年轻代(GC日志样例:[GC pause (young)])。也可能是Mixed GC来完成的,即回收年轻代又回收(部分)老年代(GC日志样例:[GC Pause (mixed)])。

  Mixed GC

    

  Before Mixed GC

被选中的Region(所有年轻代Region和部分老年代Region)已经被回收,存活的对象被压缩到深蓝色Region(最近被拷贝的老年代Region)和深绿色Region(最近被拷贝的年轻代Region)中:

   After Mixed GC

Mixed GC日志如下所示:

1 [GC pause (G1 Evacuation Pause) (mixed) 715M->682M(1024M), 0.0114279 secs]

  Mixed GC是完全STW的,它是G1一种非常重要的回收方式,它根据用户设置的停顿时间目标,可以选择回收所有年轻代,以及部分老年代Region集合(Collection Set,收集集合,简称CSet,)。在一次完整的全局并发标记周期后,如果满足触发Mixed GC的条件,那么就会执行Mixed GC,并且Mixed GC可能会执行多次(由上面的GC日志可知,并且最大次数由参数-XX:G1MixedGCCountTarget=8控制),直到CSet都被回收,并且尽可能达到用户期望的停顿时间目标。

在选定CSet后,G1在执行Evacuation阶段时,其实就跟ParallelScavenge的YGC的算法类似,采用并行复制(或者叫scavenging)算法把CSet里每个Region中的存活对象拷贝到新的Region里,然后回收掉这些Region,整个过程完全STW。

  

G1总结 

前面提到了G1模式下Evacuation阶段有两种选定CSet的子模式,分别对应Young GC与Mixed GC:

  • Young GC:选定所有年轻代里的Region。G1是通过调整年轻代Region数量来控制YGC的开销。
  • Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的部分老年代Region,在用户指定的停顿时间目标范围内尽可能选择收益高的老年代Region回收,G1就是通过控制回收老年代Region数量来控制Mixed GC的开销的。

我们可以看到年轻代Region总是在CSet内。因此G1不需要维护从年轻代Region出发的引用涉及的RSet更新。 G1的正常工作流程就是在YGC与Mixed GC之间视情况切换,大部分是YGC,Mixed GC次数少很多,背后定期做全局并发标记(满足参数-XX:InitiatingHeapOccupancyPercent条件时)。当并发标记周期正在工作时,G1不会选择做Mixed GC,反之,如果有Mixed GC正在进行,G1也不会启动并发标记周期(从initial marking开始)。

  • G1模式下的FullGC

在G1的正常工作流程中没有Full GC的概念,老年代的收集全靠Mixed GC来完成。

但是,毕竟Mixed GC有搞不定的时候,如果Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会切换到G1之外的Serial Old GC来收集整个堆(包括Young、Old、Metaspace),这才是真正的Full GC(Full GC不在G1的控制范围内),进入这种状态的G1就跟-XX:+UseSerialGC的Full GC一样(背后的核心代码是两者共用的)。

这就是为什么不建议G1模式下参数-XX:MaxGCPauseMillis=200 的值设置太小,如果设置太小,可能导致每次Mixed GC只能回收很小一部分Region,最终可能无法跟上程序分配内存的速度,从而触发Full GC。

顺带一提,G1模式下的System.gc()默认还是Full GC,也就是Serial Old GC。只有加上参数 -XX:+ExplicitGCInvokesConcurrent 时G1才会用自身的并发GC来执行System.gc();

 

posted on 2020-10-26 16:44  MaXianZhe  阅读(1899)  评论(0)    收藏  举报

导航