G1GC 全过程与核心原理介绍
G1内存布局
G1垃圾回收器内存布局【链接:1】
G1垃圾回收全过程
整体过程介绍
G1垃圾回收整体分为2个阶段,分别是仅年轻代回收阶段(young-only)和空间回收阶段(space reclamation)。这两个阶段不断交替,相互转换。在年轻代回收阶段,每次回收都会有对象升级,当年龄达到阈值,就会变成老年代对象,放到old区。当old区占比达到一个设定的阈值,就会进入空间回收阶段,释放出空间,然后又进入年轻代回收。要进行一次空间回收,前提条件是找出空间中所有的垃圾,这需要经过3个主要的步骤,分别是初始标记、回归标记和最后的清理操作。
回收过程中的关键环节如下:
- 初始标记 [STW]
- 将gc root的一级子节点标记出来,作为后续并发标记的源头
- 回归标记 [STW]
- 在并发标记的过程中,对象的引用会发生改变,因此,在并发标记结束后,需要进行一次回归标记,从而更新整个堆中的对象状态。回归标记结束,整个垃圾标记的过程也就结束了。回归标记的具体原因,后面会再进行介绍。
- 清理 [部分STW]
- 当所有的垃圾被标记出来之后,G1首先会找出那些全是垃圾的region,直接回收,放入可用集合。此外,还会在这个阶段判断接下来是否需要真正执行空间回收的操作,如果回收的空间比较多,则会执行,如果能够回收的空间比较小,就不会执行了。
G1 Full GC
在上面的介绍中,G1主要进行年轻代回收和混合回收,这些造成的停顿整体是可控的。但是如果在对象清理的过程中,发生了内存不足,那么在OOM之前,G1会启动FullGC,会进入一个停顿,并且用单线程执行回收,此时造成的停顿是不可控的,一般来讲,G1调优的最主要的目标就是要避免FullGC的出现。
重点原理介绍
G1如何进行并发标记?
G1的标记算法为三色标记,示意图如下

进行并发标记的时候,由于对象的状态是不断变化的,如果不做特殊处理,就会出现两个问题,分别是漏标和错标。
- 当标记完的对象在清理之前变为了垃圾,就会出现错标,此时这些多出来的对象被称作浮动垃圾。浮动垃圾影响有限,顶多就是下次再回收。
- 如果某些对象不是垃圾,但是因为并发问题,没有标记到,则产生漏标问题,结果就非常严重,可能导致系统崩溃。
漏标问题示意图

G1如何解决漏标问题?
G1解决漏标问题的方案是采用初始快照算法,SnapShot-At-The-Beginning(SATB)。定义如下:在标记开始的时候,对整个堆空间拍一个快照,并且此次回收的对象,就以此次快照的内容为准。这样,虽然后续会有并发修改,但是不会改变初始快照的状态,从而解决了漏标问题。
不过这个快照是虚拟的,底层实现是通过一个写屏障+链表实现的。SATB引入了write barrier技术,每当应用线程将对象赋值给引用字段时,会回调写屏障的方法。(假设将字段f设置为null,则发生f=null的赋值,此时会回调写屏障),写屏障的逻辑是,将引用原来的内容记录到一个linked list中。

当并发标记结束,jvm会stop the world,然后处理这个SATB-list,对list中的对象以及他们的引用进行标记,当标记结束,结束STW,经过这样一个过程,就可以确定,所有在最开始存活的对象都被标记为活对象了。
G1如何实现分Region回收?
垃圾回收阶段最关键的问题是要回收的区域可能被别的区域引用,存活对象的地址发生改变,原来的引用需要重新指向新的内存地址
全堆扫描回收方案
参考【1】

- 开启全堆扫描,如果扫到一个对象应用,并且这个引用指向待回收的region,则进行以下操作
- 如果这个被引用的对象没有完成拷贝,则执行这个对象的拷贝,在老对象头上标记已拷贝,更细当前的引用关系,而且把新地址记录在老对象头上。
- 如果某个对象被标记为已拷贝,则直接用记录下来的新地址,更新引用。
- 当扫描完成后,待回收region的可用对象就全部被拷贝完成,对应引用也更新完成。
基于Remembered Set扫描回收方案
参考【1】
- 什么是RSet?
- JVM的虚拟内存被划分为多个Card,每个Region也由多个Card组成
- 在G1中,每个Region有一个对应的Rset,本质是一个2^n位的bitMap,每一个bit对应一个Card
- 最开始所有的bit状态都是clean,当某个Card指向了本Region中的对象,则这个Card对应的Rset bit,会被标记为dirty。
- 通过Rset,每个Region可以知道有哪些Card指向自己,方便后续的快速分Region回收。Rset的设计思路和倒排索引比较类似。
- G1如何基于RSet回收?
- 如果要回收Region2,首先扫描GC Root,找到被引用的对象,进行迁移。然后,扫描Region2的Rset,找到所有的Card,然后扫描其中的对象,迁移这些对象的引用。
- 如何维护Rset?
- G1维护Rset的核心是Post Write Barrier和Concurrent refinement threads
- 每次如果发现有跨区域引用,则会将这个引用放入到一个队列中,当队列达到一定的阈值,则启动refinement线程,更新RSet
- 如果Rset的产生过快,refinement线程不够用,则业务线程会帮助执行,这种情况要尽量避免