G1回收器全面解析
G1回收器全面解析
最近在整理复习知识点的时候,发现对G1已经了解这么多了,趁此机会,把G1的相关设计理念给串一下
1.从阅读论文开始
如果想对G1有非常深入的一个了解的话,阅读G1的论文是必不可少的,页数也只有十几页(有用的也就七八页),而且都是概念上的知识,阅读完以后再去学习细节部分,会事半功倍。
还有一本书是源码级别讲解G1的,细节非常全,但是是日本人写的。。。我也是靠翻译器看的(不过好像日语翻译支持的不是很好)
以上两本书一本论文讲概念,一本讲G1实际的实现,如果都能研究透彻,那么你就是G1的大神人物了。
在追加一篇文章G1重要参数,里面有G1的相关参数设置,作者是oracle的垃圾回收开发者,可信度有保障(网上一些文章讲解的这些参数真的有很多误区,建议各位学习的话还是去官方途径比较好)
本篇文章需要阅读人员具备以下基础知识:
- GC roots包括哪些
- 堆空间分代模型及分代GC(年轻代、老年代)
- GC安全点和安全区域
- 三色标记(并发标记基础)
- Card Table 和 Remembered Set
- G1中的Collection Set
2.G1并发标记概述
G1并发标记的过程被很多的博客都给讲烂了,这里我结合论文描述说下我自己的理解
初始标记 :首先会进行G1中bitmap的初始设置(next 和 previous)和TAMS指针位置的处理以及更新Remeber set保障当前跨代引用的正确性,可以简单理解为初始化过程。当一些必要参数设置完成后,会进入STW,暂停所有线程,开始扫描根对象,并将根对象直接可达的对象放入mark stack(标记堆栈)中。
根分区扫描 :这个阶段是在论文中没有提到过的,但是GC的日志中是有显示的,所以重点讲一下。并发标记在实际的运行过程中都会事先伴随一次young gc。当young gc完成后,最终存活的对象会进入到Survivor中,这时对象的引用关系可能就发生了比较大的变化了,为了复用young gc的信息,该阶段会把Survivor中的对象当成根节点来扫描。
并发标记阶段 :GC线程使用三色标记并行扫描mark stack 和 satb_mark_queue 直到栈为空为止,并发标记结束。
再标记阶段 :因并发标记阶段,应用程序仍会在修改对象引用,所以标记堆栈不会完全清空,该阶段STW,用于处理最后剩下的引用,所以时间较短。
清理阶段 :这个命名是比较有歧义的,清除阶段并不是指进行垃圾回收(该阶段只会将为空的分区进行回收),而是对所有分区进行估算回收时间并排序(存活对象、预测回收时间、回收比例等),将符合条件的分区会放入Collection Set等待回收。清理阶段也会重置G1的一些数据结构,比如说将next和previous的bitmap进行交换,为下一次并发标记开始做准备。
3.G1回收细节
3.1 加快根对象扫描效率
- OopMap 根对象包括了线程栈中的引用变量,GC程序为了加速寻找这些引用变量,新增了OopMap结构用于存储当前所有线程栈中的引用变量。
- 跨代引用 由于G1是分代模型,当进行young gc的时候一定要去扫描整个老年代空间找到那些跨代的引用对象当成root节点。为了解决这个问题G1新增了Remembered Set,在young region区域上的RS记录了哪些old region中的对象引用了我,在old region区域上的RS也只记录了old region中的引用。
3.2 G1并发标记算法
G1使用了SATB + 三色标记算法来实现并发标记的功能,也是G1里比较核心的一块知识。
CMS回收器中的Increment Updated并发标记算法因其无法保证并发标记的正确性,所以会额外存在一次STW的再标记过程,而G1就改善了这一点,使并发标记的停顿时间得到显著改善。
因标记和用户程序是并发执行的,在并发过程中,对象具有漏标的风险有以下两种情况
- 对象被新创建,逻辑快照中没有该对象
- 对象引用随时在发生变化,三色标记会不准确
针对新增对象情况,G1会默认为新增对象是存活状态,不会被回收掉。
而针对引用关系的变化,G1引入了写屏障,在对象引用状态发生变化时(对象被GC标记、被修改的对象为白色标记),会将该白色对象的旧引用记录下来,保存到satb_mark_queue中。并发标记程序会定期暂停标记工作来处理satb_mark_queue中的引用变化信息,将旧引用当成root节点重新进行扫描。
其标记过程图解如下(虽然解决了漏标问题,但是会有浮动垃圾产生)
3.3 G1并发标记时辅助数据结构bitmap和TAMS
每个region上会有两个bitmap(previous和next),bitmap中使用1位表示实际内存空间的8个字节,用于指向对象的地址。每个region上还会有两个TAMS(previous 和 next TAMS)用于区分新创建的对象和正在并发回收中的对象。
初始标记阶段,会清空并初始化next bitmap,并使next TAMS指向top。
(bottom和top指针分别指向了当前region空间的起始位置和最后一个对象位置)
在并发标记过程中,region中会有新对象在不断分配,这是top指针会不断移动,next bitmap随着对象标记的进行也在不断的进行填充。(这时,next TAMS和top间的对象就是本次并发标记中新分配的对象)
在重标记阶段,应用程序暂停,标记线程会继续处理previousTAMS和nextTAMS之间的对象引用变化。
在一次并发标记的最终清理阶段,会将previous bitmap和next bitmap交换位置、previous TAMS和next TAMS交换位置,用于进行下一次并发标记。
3.4 G1中对象何时被回收
之前有讲过在并发标记阶段(mixed gc)并不会触发对象的实际回收,只是将符合条件的region加入到Collection Set。
在young gc时,会扫描所有年轻代区域(Eden + Survivor)并全部加入CSet同时预测停顿时间,只有停顿时间达标了才进行对象回收,否则先进行年轻代扩容。
如果当前模式是mixed gc模式,则不会对当前所有的CSet进行收集,而是根据停顿时间选择来一部分区域进行回收。
G1会使用复制算法对选择的Collection Set进行回收(官方术语叫Evacuation),Evacuation需要拷贝对象并修改引用,所以整个阶段都是STW。
由于G1中有浮动垃圾的存在,并且对象回收是渐进的,可能对象会把空间使用满GC还没有结束,就会触发一次full GC(full GC在Java8版本中是单线程回收 一定要注意)
3.5 GCLAB
G1采用的是复制算法,需要进行频繁的对象拷贝(这也是G1 GC耗时的主要因素,和对象的数量成正比)为了加快对象拷贝,G1使用了多线程并行拷贝。
为了减少多线程之间的冲突来提升性能,G1引入了GCLAB,每个回收线程独占一块本地缓冲区用于转移对象。
3.6 写屏障
G1所涉及到的一共有两种写屏障,一个是并发标记过程中引用变化的记录,另一个是RS跨代引用关系的维护。
为了保障并发标记过程中三色标记的正确性,G1在对象引用修改时增加了写屏障,当被修改引用的对象标记为白色时(处于并发标记周期),会把原引用加入到satb enqueue队列中,等待标记线程处理(异步操作,为了减轻主线程的开销)
为了减少入队的多线程竞争操作,原引用进入satb queue队列时,会先进入当前线程的本地缓存区,当本地缓存区放满了以后,会将本地缓存区全部加入到全局的satb队列中(该步操作要加锁保障并发安全),之后开辟新的本地缓存区存放引用关系。
RS的写屏障其实是与card table的写屏障公用的,RS更像是一种card table的扩展,在并发标记阶段应用线程更新对象引用时(有过滤条件,要是跨代引用),会先将该引用变化加入rs_queue队列,同样的,该队列存在线程本地缓存区和全局队列。
4.总结
G1的诞生是为了解决堆空间过大时回收时间慢的问题的,G1对整个堆空间进行了区域划分并且实现了一套停顿预测算法来进行渐进式的回收。既然面向大堆空间,所以一定要注意G1中full gc的问题,一旦出现后果很严重。
在1-2G的堆空间中使用G1并不会带来明显的提升,而且Java8默认的PS + PO也能保证GC在毫秒级。
G1使用了SATB + 三色标记实现了并发标记,但是后续的对象转移阶段依然是STW,所以G1的停顿时间主要和存活对象的大小成正比。
对以下几种典型业务场景的调优建议(没有给出调整参考值,因为各业务情况不同)
超频繁的对象分配
MaxGCPauseMillis停顿时间不要设置的过小,会导致young gc的延迟触发并且会使得Mixed GC回收的垃圾减少。
可以调高G1ReservePercent预留更多的堆空间用于担保分配,防止出现full gc。
最好的办法就是增加应用内存,尽可能让对象都留在年轻代被一次回收掉。
Mixed GC触发频繁,且老年代回收空间多
降低G1MixedGCLiveThresholdPercent存活空间占比设置,让更多的老年代region能够进入cset。
降低G1MixedGCCountTarget使一次mixed GC能够回收更多的region。
降低InitiatingHeapOccupancyPercent混合GC触发阈值,提前进入混合GC。
排查是否过多的创建大对象导致占用了老年代空间。