文章

JVM-G1 GC

G1回收器

  • G1 是 Garbage First 的简称,可以翻译成“垃圾优先”,它是一款面向服务器的垃圾收集器,采用标记整理算法,用于大内存的多处理器计算机,目标是实现低延时垃圾回收,从 2017年9月发布的 JDK9 开始,G1 就已经成为了默认的垃圾收集器。

回收方式

  • 年轻代回收

  • 混合回收

G1下的JVM堆内存结构

和以往的垃圾收集器不一样,G1尽管依然保留了年轻代(young generation)和老年代(old generation)的概念, 但是它们已经变成了一个逻辑上的概念,G1的堆内存被切分成若干个大小(1M ~ 32M)相同且不连续的 Region,包括 Eden,Survivor,Old Generation, Humongous。堆结构如下图

跨代引用

  • 问题:老年代对象引用了年轻代的对象,如果年轻代被引用对象被回收,则老年代使用的年轻代对象会出现问题。年轻代引用老年代的问题很容易解决。

  • 解决方式:

方式1:从GC root出发,扫面引用链,年轻代被扫描到标记为存活,性能太低。

方式2:维护一张老年代引用年轻代的记录,如果在表中被记录则不被回收。

方式3:记录记忆集(一种非回收区域对象指向回收区域的对象的数据结构):Region被哪些对象引用了,回收的时候,同样把记忆集中的对象加入GC root中。如果对象很多,记忆集空间太大。

方式三进行优化,将所有区域中的内存按一定大小划分成很多个块,每个块进行编号。记忆集中只记录对块的引用关系。如果一个块中有多个对象,只需要引用一次,减少了内存开销。

  • 卡表

每一个Region都拥有一个自己的卡表,如果产生了跨代引用(老年代引用年轻代),此时这个Region对应的卡表上就会将字节内容进行修改,JDK8源码中o代表被引用了称为脏卡。这样就可以标记出当前Region被老年代中的哪些部分引用了。那么要生成记忆集就比较简单了,只需要遍历整个卡表,找到所有脏卡。

  • 使用写屏障维护卡表

JVM使用写屏障(write Barrier)技术,在执行引用关系建立的代码时,可以在代码前和代码后插入一段指令 从而维护卡表。 记忆集中不会记录新生代到新生代的引用,同一个Region中的引用也不会记录。G1使用写屏障技术,在执行引用关系建立的代码执行后插入一段指令,完成卡表的维护工作。

记忆集的生成流程分为以下几个步骤:

1、通过写屏障获得引用变更的信息。

2、将引用关系记录到卡表中,并记录到一个脏卡队列中。

3、JVM中会由Refinement 线程定期从脏卡队列中获取数据,生成记忆集。不直接写入记忆集的原因是避免过多线程并发访问记忆集

年轻代回收

该阶段为STW

1.Root扫描寻找GC root,将所有的静态变量、局部变量扫描出来。

2. 处理脏卡队列中的没有处理完的信息,更新记忆集的数据,此阶段完成后,记忆集中包含了所有老年代对当前Region的引用关系。

3、标记存活对象。 记忆集中的对象会加入到GC Root对象集合中, 在GC Root引用链上的对象也会被标记为存活对象。

4、根据设定的最大停顿时间, 选择本次收集的区域,称之为回收集合Collection Set。

5、复制对象: 将标记出来的对象复制到新的区中,将年龄加1,如果年龄到达15则晋升到老年代。老的区域内存直接清空。

6、 处理软、弱、虚、终结器引用,以及JNI中的弱引用。

从GC root和记忆集中的对象出发扫描引用链的对象。

混合回收

多次回收之后,会出现很多old老年代区,此时总堆占有率达到阈值(默认45%)时会触发混合回收MixedGc 混合回收会由年轻代回收之后或者大对象分配之后触发,混合回收会回收 整个年轻代 +部分老年代。 老年代很多时候会有大量对象,要标记出所有存活对象耗时较长,所以整个标记过程要尽量能做到和用户线程并行执行。

  • 步骤

1、初始标记,STW,采用三色标记法标记从GC Root可直达的对象。

2、并发标记,并发执行,对存活对象进行标记。

3、最终标记,STW,处理SATB相关的对象标记。

4、清理,STW,如果区域中没有任何存活对象就直接清理。

5、转移,将存活对象复制到别的区域。

  • 初始标记

1.初始标记会暂停所有用户线程,只标记从GCRoot可直达的对象,所以停顿时间不会太长。采用三色标记法进行标记三色标记法在原有双色标记(黑也就是1代表存活,白代表可回收)增加了一种灰色,采用队列的方式保存标记为灰色的对象。

  • 并发标记

1.接下来进入并发标记阶段,继续进行未完成的标记任务。此阶段和用户线程并发执行。从灰色队列中获取尚未完成标记的对象B。标记B关联的A和C对象,由于A和C对象并未引用其他对象,可以直接标记成黑色,而B也完成了所有引用对象的标记,也标记为黑色。最后从队列获取C对象,标记为黑色,也标记为黑色。所以剩余对象F就是白色,可回收。

2.三色标记存在一个比较严重的问题,由于用户线程可能同时在修改对象的引用关系,就会出现错标的情况,比如: 这个案例中正常情况下,B和C都会被标记成黑色。但是在BC标记前,用户线程执行了 B.c= null;将B到c的引用 去除了。同时执行了A.c=c;添加了A到c的引用。此时会出现错标的情况,c是白色可回收。

3.G1为了解决这个问题,使用了SATB技术(Snapshot At The Beginning, 初始快照)。SATB技术是这样处理的:

1、标记开始时创建一个快照,记录当前所有对象,标记过程中新生成的对象直接标记为黑色。

2、采用前置写屏障技术,在引用赋值前比如B.c= null之前,将之前引用的对象c放入SATB待处理队列中。SATB队 列每个线程都有一个,最终会汇总到一个大的SATB队列中

  • 最终标记

最终标记会暂停所有用户线程,主要是为了处理SATB相关的对象标记。这一步中,将所有线程的SATB队列中剩余的数据合并到总的SATB队列中,然后逐一处理。SATB队列中的对象,默认按照存活处理,同时要处理他们引用的对象。SATB的缺点是在本轮清理时可能会将不存活的对象标记成存活对象,产生了一些所谓的浮动垃圾,等到下一轮清理时才能回收。

  • 转移

转移的步骤如下:

1、根据最终标记的结果,可以计算出每一个区域的垃圾对象占用内存大小,根据停顿时间,选择转移效率最高(垃圾

对象最多)的几个区域。

2、转移时先转移GC Root直接引用的对象,然后再转移其他对象。

为什么G1能代替CMS

思想决定高度,对于垃圾回收器也一样,G1 的三个优秀的设计思想,为后面的垃圾收集器(ZGC)奠定了坚实的基础:

1.基于 Region 的内存布局

2.面向局部收集的设计思想

3.GC停顿时间和吞吐量的平衡

Region化整为零,面向局部收集的思想完全碾压了 CMS这种需要收集整个老年代的设计。基于 Region可以同时兼顾年轻代和老年代的回收,而 CMS只能回收老年代。基于 Region,因为回收的粒度更细,范围更小,使得 G1的停顿时间更加可预测。

License:  CC BY 4.0