G1收集器

一、G1简介
G1是jdk7推出的垃圾收集器,是jdk9之后的默认收集器,相当先进。相比大家所熟知的垃圾收集器例如serial,serial old,parnew,parallel scanvenge,parrallel odl,cms,G1有如下特点:

并行与并发
serial old与parrallel old在收集老年代时,在gc工作过程中需要STW(stop the world),执行并发标记,这个阶段往往是很耗时的,而G1可以在这个环节让gc线程与用户线程并行,减少用户线程的停顿。这个特点类似CMS,但是两者的实现却是截然不同的。

分代收集
G1仍然使用分代收集,但是新生代与老年代不在是物理上隔离的,它们分别由若干个Region组成。G1可以独立管理整个堆,包括对新生代和老年代的收集。

空间整合
CMS使用的是标记-清除算法,这样会导致很多内存碎片,不利于对象的分配。而G1在收集时实际上是将存活对象拷贝到另一个Region,所以垃圾回收之后的内存是规整的。

可预测的停顿
可以指定一个参数,给出GC停顿时间的期望值,G1会尽可能使得GC停顿时间不超过这个值。(-XX:MaxGCPauseMillis=n)
上面这段话引自深入理解JVM这本书,并且加入了一点点个人理解,在第一次看到这段话时,我也是非常迷惑的。在读了大量的文章之后才初步理解了这四个特点,阅读完下文之后再回过头来看这四点,相信会清晰不少。

二、G1概览

G1的内存结构
G1将堆区划分成了2048个Region,每个Region大小在1M ~ 32M之间。Eden区,survivor区,Old区,他们各自占有一部分Region。还有Humongous区,用于存放大对象,当一个对象的尺寸超过半个Region的大小时,会将这个对象分配在H区,一个H Region放不下则需要多个连续的H Region来存放。


G1的两种GC模式
G1中有两种GC模式,分别是Young GC和Mixed GC。Young GC就是回收年轻代,将Eden区存活的对象和Survivor区存活的对象copy到空闲的Region中,整个过程STW,Young GC的执行速度也比较快,这不是本文的重点。Mixed GC回收年轻代所有Region加上部分老年代Region,回收老年代有几个步骤:初始标记,并发标记,最终标记,筛选回收。本文的重点也即解释回收老年的这四个步骤,以及其中的一些实现细节,也正是这些实现细节决定了文章开头所写的G1那四个特点。

三、初始标记(inital marking)
这个阶段仅仅标记GC Root能直接关联的对象,并修改TAMS的值,使得下一步并发标记时新分配的对象能够在正确的区域中分配。这个步骤需要STW,但是过程很短暂,一般这个步骤会搭车到Young GC的时候执行,因为Young GC的时候是STW的。

如何找到GC root?
在这里我们考虑一个问题,如果进行年轻代gc,是不是需要将整个老年代全部扫描一遍,看看是否有老年的引用指向了年轻代的对象,如果有,那么该对象就成为了一个gc root。老年代一般都比较大,这样扫描做的成本太高了。于是可以考虑将那些持有年轻代引用的老年代对象记录下来,这样在年轻代gc时,就不用进行全堆扫描,但是这样会消耗很多的空间。

card table
card table作为一个折中方案应运而生,它将Region划分成512byte大小的card,通过一个位来标识该card的状态,如果这个card中有引用指向了年轻代的对象,则标记置为1,如果没有那么默认就是0。card table告诉你某个card区域有对象持有年轻代对象的引用,但无法告诉你具体哪些对象有指向年轻代对象的引用。这样就只需要到标记为1的card区域去找GCRoot了。card table是兼顾了空间成本与GC效率。

Rset
前面提到G1在进行Mixed GC时是回收部分Region的,那么在回收单个Region时,如何兼顾效率与正确性呢?于是有了Rset(Remembered set)每一个Region有一个Rset,它是一个Map,key是Region编号,value是card编号的集合。意思是当前Region的对象被其他哪些Region所引用了,且引用的位置在哪些card中。在定位GC root时,就到当前Region的Rset中去找,将Rset中的每个Region对应的card都扫一遍。通过card table与Rset,可以快速的找到当前Region的GC Root,不仅提高GC Root的效率,并且可以实现独立Region进行回收,而不受其他Region引用关系的干扰。这也是Young GC和Mixed GC能够高效定位GCRoot的原因。
假设Region2、Region3属于老年代,Region1为年轻代,Region2的card1和card2中都有引用指向了Region1,Region3的card7和card9都有引用指向了Region1。在收集Region1时,只需要根据Region1的Rset,到Region2的card1 card2和Region2的card7 card9中找GCRoot就行了,避免大量无用的扫描。


四、并发标记(concurrent marking)
第一步的初始标记为我们定位到了GCRoot,接下来的并发标记便是从这些GCRoot出发,沿着引用关系进行遍历,扫描整个对象图,找出存活的对象。这个阶段耗时比较长,但是能够与用户线程并发执行。可达性分析算法大家肯定都知道,如果只有GC线程工作肯定是没问题的,但是现在需要GC线程与用户线程并发执行,这就有问题了。因为用户线程的执行会导致对象之间的关系发生变化,由此导致两个问题:浮动垃圾和对象消失。下面来分析一下,这两种问题的产生和解决方案。

三色标记法
根据对象被垃圾收集器的访问情况,可以将对象分成三种类型。如果一个对象未被访问,则为白色;如果一个对象被访问,但其直接引用对象还没全部被访问,则为灰色;如果一个对象被访问,且直接引用对象也全部被访问,则为黑色。


浮动垃圾
垃圾收集器在扫描了ABC三个对象。如果在真正回收之前,A释放了对B对象的引用。那么BC其实已经不是存活对象了,应当被回收的,但是在本次GC过程中BC不会被回收,即产生了浮动垃圾。浮动垃圾产生的原因是在垃圾收集器扫描了对象之后,对象被释放了。并发标记出现浮动垃圾的问题是很常见的。


对象消失
并发标记的第二个问题是对象消失的问题,相比浮动垃圾而言,对象消失是会影响GC的正确性的,假如一个还在使用的对象被回收了,这将直接导致程序出错。先来看看对象消失是如何产生的。假设灰色对象B断开了对对象C的引用,而后,对象A由引用了对象C。垃圾回收器只能扫描到AB,C由于引用的变动已经无法被扫描到了,因此被认为是垃圾,而其实C是被A所引用的,这就是对象消失问题。


对象消失的产生有两个必要条件:1.白色对象仅被灰色对象引用,且所有灰色对象断开了对该白色对象的引用。2.该白色对象重新被黑色对象所引用。要解决对象消失的问题,就需要打破这两个条件。于是产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning SATB)。

增量更新
增量更新破坏的是第一个条件,当黑色对象引用了白色对象时,将这个白色对象记录下来,并发标记结束后再根据记录的白色对象扫描一次,即白色对象会在第二次扫描时成为一个灰色对象,被黑色对象引用的白色对象会被扫描到,这样就不会消失了。增量更新的基本思想是,把满足上述条件的新增的对象引用再扫一遍。CMS就是用的增量更新这种方式。

SATB
原始快照破坏的是第二个条件,当灰色对象要删除对白色对象的引用时,就会将这个引用记录下来。并发标记结束后再根据记录的引用扫描一次。SATB的基本思想是,按照当扫描到一个对象时,记录下改对象的引用快照信息。也就是说,在标记之初,如果一个对象是活的,那么就认为它就是活的,尽管它有可能是真的垃圾。G1是用的SATB。

write barrier
增量更新和write barrier对垃圾的判定都是“宁可放过,不可错杀”,他们都是基于写屏障去实现的。写屏障可以看作是JVM层面对“引用类型字段赋值”的AOP,增量更新则是利用写屏障“后置通知”来记录新引用的对象,而SATB则是利用写屏障“前置通知”来记录对象原始快照。

新对象的分配
到此为止,我们解决了并发标记中的对象消失问题,那么在并发标记时,G1是如何处理新分配的对象的呢?上文提到在初始标记阶段会修改TAMS的值,使得下一步并发标记时新分配的对象能够在正确的区域中分配。第一次读这句话肯定是有疑问的,接下来的内容将会对这句话做个解释。

TAMS
TAMS是top at mark start的简称,TAMS记录的是在一次并发标记开始时,top指针的位置。也就是说在并发标记开始时,把TAMS指向top的位置。在并发标记过程中,用户线程会创建新的对象,那么top指针将逐渐向后移动,而TAMS保持不动,top与TAMS之间的空间就是新对象分配的区域。有两个TAMS,PreTAMS是上一次并发标记开始时top的位置,NextTAMS是本次并发标记开始时top的位置。PreTAMS到NextTAMS是上次并发标记开始到本次并发标记开始这个过程中新对象分配的空间,NextTAMS之前的区域是本次进行并发标记的区域。


Bitmap
Bitmap是一个标记数组,在并发标记的过程中,存活对象的位置将被标记,并发标记的过程实际上就是在改变Bitmap中的值。有两个Bitmap,PreBigmap记录了上一次并发标记的结果,而NextBitmap是本次并发标记正要记录结果的地方。


五、最终标记(final marking/remarking)
最终标记,对用户线程做一个短暂的停止,用于处理并发标记阶段结束后遗留的SATB(关于SATB上面有介绍)。根据SATB做最终标记,防止对象消失。


六、清理(clean up)
清理,这个阶段时暂停的,并不是在堆中实际的清理对象,而是在 bitmap里统计每个region被标记为存活对象有多少。如果发现有完全可回收的Region,则直接将其加入到可分配列表中。这个阶段NextBitmap和PreBitmap互换,nextTAMS和preTAMS互换,为下一次并发标记做准备。
文章开头提到G1的一个特定:可预测的停顿模型。G1会尽量将收集时间控制在预期值内,G1会优先选择回收价值比较高的Region,而G1做选择的依据跟此处的统计数据不无关系。


七、Mixed GC流程
三、四、五、六这四段介绍了Mixed GC的标记流程。下面按照整个流程,根着图再来走一遍。
图A,初始标记。在初始标记阶段,NextTAMS指向top,NextTAMP界定了本次并发标记的范围,即NextTAMS之前。在这个阶段通过Rset可以快速定位到当前Region的GCRoot,初始标记阶段是STW的,但是很短暂,并且可以搭在Young GC阶段执行。

在这里插入图片描述

图B,并发标记与最终标记。并发标记的过程实际上是在维护NextBitmap,标记出存活的对象。这个阶段耗时较长,但GC线程与用户线程可以并发执行。随着新对象的分配,Top会逐渐向后面移动,top与NextTAMS之间的空间就是并发标记过程中新对象占用的空间。并发标记会产生浮动垃圾和对象消失的问题,浮动垃圾只能到下一次GC被标记,而对象消失的问题是在最终标记阶段解决的。解决的方案有两种,增量更新和原始快站SATB。G1采用的SATB,并发标记结束后会进行最终标记,最终标记就是根据SATB的记录再次进行标记,这个阶段是STW的,但是也比较短暂。

在这里插入图片描述

图C,清理。并不是进行整真正的标记-清除,而是统计Region中存活对象。在这个阶段,PreTAMS和NextTAMS互换。preBitmap与nextBitmap互换,为下一次标记做准备。这个过程也是STW的。

在这里插入图片描述

图D,下一次GC的初始标记。将NextTAMS指向top,NextBitmap置空为并发标记做准备,根据Rset定位到GCRoot。

在这里插入图片描述

图E,下一次GC的并发标记与最终标记。可以看到PreBitmap记录了上一次标记的结果。并发标记过程中,NextBitmap逐渐写入信息,T随着新对象的产生,top与NextTAMS之间的距离逐渐拉大。同样,在并发标记之后进行最终标记,防止对象消失。

在这里插入图片描述

图F,清理。同图C。

在这里插入图片描述

到此为止,整个MixedGC的标记过程就基本上走完了,其实就是在不同重复ABC/DEF这个过程。跟着图,多走几遍就能理解了。

八、总结
G1中,Young GC没有什么特别的地方,主要是MixedGC的标记过程稍微复杂点。那么站在GC回收的全局视角,我们再次来回顾一下G1的垃圾回收。对于Young GC,G1会选择其所有的Region,将存活对象拷贝到另外若干空闲的Region,本质上还是复制算法,只不过年轻代是由多个Region组成,Young GC过程是STW,但是通过Rset定位GCRoot快,Young GC存活对象少,拷贝快,整个过程不会持续很久。对于Mixed GC,它主要分成全局并发标记和拷贝存活对象两个流程。全局并发标记又分为四个阶段,第一是初始标记,通过Rset定位GCRoot,这个过程很快且需要STW,一般会在Young GC时,顺带执行Mixed GC的初始标记,这样就省去了额外的停顿。第二是并发标记,虽然耗时长,但用户线程与GC线程并行执行。第三是最终标记,防止对象消失。第四是清理,统计存活对象。接下来就是第二个流程,拷贝存活对象。与Young GC类似,STW,根据期望停顿时间,来筛选部分回收价值高的老年代Region以及全部的年轻代Region,将他们拷贝到对应分代的空闲Region中。

文章末尾,我们在回过头来理解一下《深入理解Java虚拟机》上介绍的G1垃圾回收器的四个特征。并行与并发:主要体现在MixedGC的并发标记上,YoungGC回收频次高,时间短。MixedGC涉及的内存空间大,回收频次低,时间长。而MixedGC消耗时间的大头又在并发标记上,G1做到并发标记与用户线程并行,显然能够减少用户线程的停顿。分代收集:G1中依然保留了原始的分代特征,只不过每个代划分成了若干个Region,依然实行分代收集。空间整合:早先并发收集器CMS使用的是标记清除算法,会产生大量内存碎片,不利于对象的分配。在G1中,YoungGC与MixedGC均是将存活对象拷贝到空闲Region。在每一个Region内部看来,内存都是规整的。可预测的停顿:使用G1时,可以指定期望的停顿时间(-XX:MaxGCPauseMillis=n),G1尽量保证停顿时间不超过这个值。因为G1只是对部分回收价值高的Region做处理。

G1没有Full GC,对老年代的收集全靠MixedGC。如果MixedGC速度跟不上对象分配的速度,导致old填满,无法进行MixedGC。那么会切换成Serial Old来进行Full GC,这时候就可能产生长时间的停顿。所以在使用G1时,不能将期望停顿时间设置的太低,否则会触发FullGC,从而引发更大的停顿。以上便是我对G1的理解,欢迎大家指正和讨论!

参考
《深入理解Java虚拟机》
https://juejin.im/post/6844904070788939790
https://zhuanlan.zhihu.com/p/110079401
https://hllvm-group.iteye.com/group/topic/44381
https://blog.csdn.net/wanghang96/article/details/109731479

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值