G1原理—G1垃圾回收过程之Mixed GC

128 阅读45分钟

1.Mixed GC混合回收是什么

(1)YGC的过程

一.暂停系统程序运行

二.选择需要回收的Region

三.标记所有存活对象

四.复制对象操作

五.清理新生代所有垃圾释放Region

 

(2)Mixed GC有那些步骤

Mixed GC(混合回收)主要有以下步骤:

 

步骤一.初始标记阶段

标记出所有由GC Roots(及RSet)直接引用的对象,会STW暂停程序运行。

 

步骤二.并发标记阶段

标记出"在初始标记阶段标记的所有对象"所引用的对象,不会STW。

 

步骤三.最终标记阶段

这个阶段是标记出"在并发标记阶段中没有被标记到的所有对象",这些对象主要就是程序还在运行时产生的一些新对象,会STW。

 

步骤四.存活对象计数

对每个Region区域中被标记的对象进行计数,即统计出每个Region存活对象的数量和垃圾对象的数量。

 

步骤五.垃圾回收阶段

需要Stop the World,这个阶段会选择性价比较高的Region。把这些Region的存活对象复制到新分区,然后回收掉选择的那些分区。

 

前面分析YGC的转换关系时介绍过:在YGC之后,有可能会进入Mixed GC并发标记阶段。Mixed GC在某次YGC之后,就直接开始进入并发标记阶段了。YGC、Mixed GC、FGC间的过程转换如下所示:

(3)YGC和Mixed GC的关系

在YGC时,会有GC Roots(局部变量 + 静态变量)标记这个步骤。在YGC的对GC Roots起始点进行标记的步骤中,会把GC Roots直接引用的新生代对象 + 新生代的RSet作为起始点去标记。把新生代的RSet作为起始点是因为老年代有些定时任务可能创建新对象。

 

说明:GC Roots(直接/间接)引用 = 被GC Roots(直接/间接)引用

 

但实际上GC Roots中并没有新生代、老年代的区分,所以在新生代进行标记时,会把所有GC Roots直接引用的对象都进行标记,而进行处理时只会处理新生代。

 

YGC其实是Mixed GC的前奏,或者说YGC是Mixed GC的初始标记阶段。从YGC、Mixed GC、FGC间的过程转换关系图可知,某次YGC后可能会直接进入Mixed GC的并发标记阶段。所以当某次YGC结束后开启Mixed GC的并发标记,那么就代表了Mixed GC已经完成了初始标记阶段,这时可理解为YGC已处理完Mixed GC初始标记阶段的工作了。

 

(4)Mixed GC的并发标记是从那些对象开始的

一.如何找出新生代的所有存活对象

Mixed GC的并发标记阶段逻辑上应该是:把GC Roots直接引用的对象的字段全部都遍历一遍。从而找到所有的间接引用的存活对象,然后打上存活标记。这样就可以找出GC Roots直接引用 + 间接引用的所有新生代的存活对象。

 

但实际上Mixed GC中的并发标记阶段的实现却并不是这样。由于YGC会找出GC Roots引用的新生代对象以及RSet引用的新生代对象,所以YGC后所有被GC Roots直接引用 + 间接引用的存活对象都在S区了,并且老年代引用的新生代存活对象也全部在S区了。那么此时还剩下哪些对象需要全部找出来呢?此时还需要找出剩下的老年代的所有存活对象。

 

二.如何找出老年代的所有存活对象

首先在GC Roots中直接引用的对象:有一部分会在新生代中,还有一部分会在老年代中。所以可以从GC Roots中直接获取直接引用的老年代对象,这样就可以确定Mixed GC并发标记的起始点了:Survivor区的对象 + GC Roots直接引用的老年代对象 + 老年代的RSet。

 

注意,Mixed GC初始标记的起始点(=YGC并发标记的起始点)是:GC Roots直接引用的新生代对象 + 新生代的RSet。

 

为什么要加上老年代的RSet?因为Mixed GC的并发标记阶段不可能把整个老年代都遍历一遍。所以便可以用RSet作为起始点,找到跨分区引用的引用链,从而找到存活对象。

 

(5)总结

Mixed GC混合回收是什么:

一.YGC的过程

二.Mixed GC有那些步骤

三.YGC和Mixed GC的关系

四.Mixed GC的并发标记是从那些对象开始的

五.Mixed GC的标记还需要做哪些内容

 

问题:Mixed GC(混合回收)的标记还需要做哪些工作?由于需要从老年代中选择一些性价比较高的区域来回收,所以需要进行性价比相关的标记。那么性价比又应该怎么计算?在MGC的并发标记阶段中会使用位图,那么位图在并发标记阶段又应该怎么使用?

 

2.YGC可作为Mixed GC的初始标记阶段

 

(1)YGC流程的一些细节调整

前面的YGC流程在最后一步才判断是否开启并发标记,但这是不准确的。实际上,在YGC开始时就会判断一下本次是否要尝试开启并发标记。因为如果当前老年代占用 + 本次分配对象已经超过45%的堆内存,那么大概率在YGC最后一步就要开启并发标记了。所以这时在YGC开始时就可以直接设置一个Flag标记表示可以开启,在YGC过程中就可以根据这个Flag标记提前并行把一些前置的工作做了。然后在YGC结束时,结合其他的判断条件,再尝试启动并发线程即可。

 

所以在YGC开始时判断是否要开启并发标记的一个原因:通过设置一个Flag标记,方便后续根据标记来执行一些处理(如前置工作)。

 

(2)YGC开始时判断是否开启并发标记的原因

由于Mixed GC并发标记的起始点是:Survivor区的对象 + GC Roots直接引用的老年代对象 + 老年代的RSet,而且YGC又是Mixed GC的前置部分。

 

所以在YGC开始时判断是否要开启并发标记,就能决定是否可以提前并行地把GC Roots引用的老年代对象进行处理,处理好的这些老年代对象就可以在Mixed GC的并发标记线程启动时使用。

 

因此在进行YGC的GC Roots标记时,其实还会有不同的情况。当发现不需要开启并发标记时,会进行一套操作。需要开启并发标记时,会进行另一套操作(记录直接引用的老年代对象)。

 

简单来说,在YGC开始时判断是否要开启并发标记的另外一个原因:判断YGC在处理GC Roots时是否要关注和处理直接引用的老年代对象。如果需要开启并发标记,那么就关注处理,否则就不用关注处理(注意:GC Roots是不区分新生代和老年代的)。

 

YGC开始时判断是否要开启并发标记的原因:

原因一:通过设置一个Flag标记,方便后续根据标记来执行一些处理(如前置工作)。

原因二:判断YGC在处理GC Roots时是否需要关注和处理直接引用的老年代对象。

(3)YGC可以作为Mixed GC的初始标记阶段

在YGC阶段,首先会判断是否需要开启并发标记。然后被GC Roots引用的新生代对象,会被放入到Survivor区中。

同时,如果发现需要开启并发标记,那么一些被GC Roots直接引用的老年代对象,也会被记录下来。

那么最终所有的GC Roots直接引用的对象一定都会被标记出来,这样就可以把Mixed GC初始标记过程中所有需要的对象全都标记出来了,因此YGC可以做为Mixed GC的初始阶段。

 

(4)借助Survivor + GC Roots记录即可完成老年代的标记

在YGC中,当发现需要开启并发标记,当记录下被GC Roots直接引用的老年代对象后,此时除了可以对引用了老年代对象的GC Roots做一些特殊处理之外,还可以把Survivor对象 + GC Roots引用的老年代对象 + 老年代RSet,作为Mixed GC时执行并发标记的起始点。因为以它们作为并发标记的起点,一定可以找到所有老年代的存活对象。

(5)总结

为什么YGC可作为Mixed GC的初始标记阶段?

一.YGC流程的一些细节调整

二.YGC开始时判断是否开启并发标记的原因

三.YGC可作为Mixed GC初始标记阶段的原因

四.借助Survivor + GC Roots记录即可完成老年代的标记

 

问题:YGC是需要对所有新生代空间进行全部遍历然后回收的,那么Mixed GC是否需要对整个老年代进行回收?如果不需要,应如何避免遍历整个老年代?

 

答:Mixed GC不需要对整个老年代进行回收,但是Mixed GC需要对整个老年代进行全部标记。因为要挑选出性价比高的Region进行回收,不标记全部就无法挑选出性价比较高的Region。

 

3.Mixed GC并发标记算法详解(一)

(1)初始标记阶段给Mixed GC带来了什么

根据前面的介绍可知,初始标记阶段,给了Mixed GC两块区域的内容:

一.Survivor区的存活对象

二.GC Roots直接引用的老年代对象集合

 

通过这两组存活对象以及RSet的跨分区引用关系,Mixed GC就可以完成所有存活对象的标记。

虽然这些起始点对象都拿到了,可以通过这些起始点对象一个一个找到所有的存活对象。

 

但是因为是并发标记,程序还在运行,所以并发标记过程中系统程序还会产生一些新的对象,这些新的对象并不在并发标记起始点对象的范围内,那应如何判断这些新的对象是否是存活对象。

 

如下图示,在并发标记的过程中,新产生了一些对象。虽然针对这些对象进行一些增量标记即可,但应怎么做这个增量标记?G1的并发标记阶段又是怎么处理的呢?

(2)对增量对象处理的一个简单思路

一个简单思路:在并发标记阶段,新创建的所有对象,都存储到一个额外开辟的空间里。并发标记阶段结束后,通过额外开辟空间里的对象,标记所有存活对象。

 

但是这个思路肯定不太合适,因为需要额外的空间,而且如果程序运行的并发量比较高,那么性能也会比较差。此外这些对象还可能造成原先被标记过的那些对象的引用关系发生变更,从而需要额外维护这些新对象变更的引用关系。

(3)对象分配的特性——连续性

由于分配对象时会使用指针碰撞法分配对象:

一个top指针代表TLAB已经使用到的内存;

一个objSize代表分配对象需要的内存大小;

一个end指针代表TLAB所占内存的结束位置;

那么分配一个对象时占用的内存位置就是:objSize + top,这说明进行对象分配时一定是线性的、连续的。

 

同时TLAB的分配和对象的分配,其实使用的都是同一块代码。TLAB的分配和对象的分配,都使用了指针碰撞法。

 

所以在同一个Reigon分配对象时,Region中已使用的内存一定是连续的,如下图示:

可见,不管在TLAB中还是在Region中,内存的使用一定是连续的。如果有空的内存,就会使用dummy对象进行填充。如果要记录并发标记阶段新产生的对象,则可利用这种连续分配的特性。

 

(4)使用指针法来确定增量对象的范围

如果并发标记进行了一段时间,在某一个Region里,新对象以及已经被标记的对象会如下所示:

其中红色的是已被标记的对象,绿色的是新产生的对象。红色表示的已被标记的对象不一定存活,只是说明被标记而已。

 

在并发标记开始到结束过程中,这个Region的top指针一直指引着内存最终使用的位置。

 

于是G1就可以在并发标记后,对Region这段标记前到标记后top位置变换的区域进行遍历标记,这样就可以实现标记剩余的存活对象了。

从图中可以看出,当并发标记结束后:直接以并发标记前的top位置作为起始点,遍历到并发标记后的top位置。这样就可以找到所有新创建的对象,来判断其是否存活。

 

(5)总结

Mixed GC并发标记算法包括:

一.初始标记阶段给Mixed GC带来了什么

二.对增量对象处理的一个简单思路

三.对象分配的特性—连续性

四.使用指针法来确定增量对象的范围

 

问题:如何提升标记效率?Mixed GC相当于需要遍历全部堆内存里的对象,如果对每个对象打一个标签是否可行?如果不打标签那么在垃圾回收时怎么办?继续遍历对象吗?

 

给每个对象打标签不可行,因为要开辟额外的存储空间来进行专门存储。其实可以借助一些额外的存储结构来描述,比如使用位图Bitmap。

 

4.Mixed GC并发标记算法详解(二)

(1)为什么要使用位图

已知一个Region是一块连续的内存,并且Region里面的对象分配、TLAB分配也都是连续的,如下图示:

基于上图进行如下分析:

如果没有位图,那么在每次标记时,对于一个Region的处理,只能是从头开始进行标记。因为不知道这个Region到底哪些地方被标记了、哪些地方没有被标记,即使使用了top指针、end指针,能找到并发标记开启前的对象使用位置,也无法知道具体哪些对象被标记了、哪些没有被标记。要想知道具体哪些对象被标记了,还是只能重新遍历所有对象。因此才需要一组数据告诉G1:哪些内存被使用了、其中的对象是否被标记了、标记的状态又是什么。

 

所以为什么要使用一个额外的数据结构来描述标记情况?其实就是为了快速拿到标记情况,或者根据标记情况来快速进行清除操作。否则如果不记录标记情况的话,那么即使标记过,每次要使用时也只能通过遍历的方式获取。所以才需要把标记情况记录下来,而位图就是比较合适的数据结构。

这样只要在BitMap标记好了,并且没有新的改变,那么每次都可以从BitMap中快速拿到标记的数据来直接使用。

 

(2)与位图相关的指针(bottom指针 + prev指针 + next指针 + top指针)

前面已经介绍了top指针和end指针,其实关于位图和并发标记过程的内存情况,还有另外几个指针。

 

这几个指针分别代表的Region地址含义如下:

bottom指针:Region中内存使用的起始地址

top指针:Region中内存使用的结束地址

prev指针:上一次并发标记后处理到的地址

next指针:这一次并发标记开始前已经使用的内存结束地址

end指针:Region的结束地址

 

指针总结:

一.bottom和end就是Region的起止地址

二.top指针会一直跟随对象分配的最新地址

三.prev指针是上一次并发标记处理到的地址

并发标记过程是有可能失败的,这也是为什么会有多次并发标记的原因。即每一次并发标记,并不一定都能走到最后,成功完成一次并发标记的。所以就有可能会出现标记了一半,然后标记被终止的情况。因此这个prev指针就是记录上一次执行并发标记时,标记到的位置。

四.next指针

每次并发标记开始时,next指针就会指向top。

并发标记开启后:next指针会一直不变,而top指针会随着新对象创建而不断移动。那么从next指针到top指针这段内存,就是并发标记过程中新对象的内存。下一次并发标记开始时,next指针又会指向top。

(3)G1引入了哪些位图(PrevBitMap位图 + NextBitMap位图)

一.G1引入的两个位图(PrevBitMap位图和NextBitMap位图)

第一个位图叫PrevBitMap,记录了从bottom指针到prev指针之间的内存区域里所有对象的标记状态。也就是记录了上一次并发标记时标记到的对象内存范围,如下图示。这些标记状态会包括是否标记、是否存活等。

第二个位图叫NextBitMap,记录了从bottom指针到next指针之间的内存区域里所有对象的标记状态,如下图示:

NextBitMap记录了本次并发标记过程中:整个Region从开始到开启并发标记那个时刻的所有对象的标记状态,也就是bottom指针到next指针这个范围内的内存使用的标记状态。

 

二.为什么有了NextBitMap位图还需要一个PrevBitMap位图

因为假如本次并发标记开始时,发现上次并发标记失败了,那就意味着:本次并发标记要从bottom指针开始进行标记,一直标记到next指针的位置。虽然上次并发标记是失败了,但依然做出了一部分标记操作。所以本次并发标记其实还能继续使用它,这就是需要PrevBitMap的原因(类似于断点续传)。

 

有了NextBitMap还需要PrevBitMap的原因总结:如果进行了多次并发标记,那么每次进行最新的并发标记时,都可以接着上次并发标记的内容继续标记,而不需要重新遍历,从而大大节省了时间。如下图示:

(4)总结

Mixed GC并发标记算法包括:

一.初始标记阶段给Mixed GC带来了什么

二.对增量对象处理的一个简单思路

三.对象分配的特性—连续性

四.使用指针法来确定增量对象的范围

五.为什么要使用位图

六.与位图相关的指针(bottom指针 + prev指针 + next指针 + top指针)

七.G1引入了哪些位图(PrevBitMap位图 + NextBitMap位图)

 

5.Mixed GC并发标记算法详解(三)

(1)进行第一次并发标记前(已完成初始标记)

此时bottom指针指向Region的起始位置,top指针指向当前已经使用的内存的结束位置。

那么此时PrevBitMap是空的,NextBitMap也是空的,并且prev指针的位置和bottom指针的位置是一样的,而且next指针的位置和top指针的位置是一样的。如下图示:

(2)进行第一次并发标记

此时,程序会不断运行创建新的对象,同时并发标记也开始进行。那么位图会不断被补充,并且top指针会不断移动。因为这是第一次标记,PrevBitMap是不需要发生任何改变的,只需要在NextBitMap中标记信息即可。如下图示,在标记进行的同时,prev指针也会不断的迁移。

假如此时标记失败,那么只能进入到下一次并发标记。此时就会把NextBitMap给到PrevBitMap,用于下一次并发标记使用。

(3)进行第二次并发标记

进行第二次并发标记时:next指针会变化到top指针的位置,prev指针会指向上一次并发标记时指向的位置。

那么此时NextBitMap就可以使用PrevBitMap中的一些标记信息了。

然后继续并发标记的后续过程。首先会在NextBitMap中,把prev后next前的所有对象都给标记上。接着由于还在不断创建新对象,所以会继续移动top指针。

此时并发标记就接近尾声了。

 

但还有一个问题:这一次的并发标记和上一次的并发标记,对PrevBitMap中标记的标记状态是有可能不一致的。比如上一次标记是被终止的,终止后PrevBitMap标记的对象很可能更改引用关系或从存活变垃圾。所以实际的状态很可能是PrevBitMap和NextBitMap出现了不一致,如下图示:

那么应该如何解决这个问题?怎么才能沿用上一次标记的状态,同时又能保证标记是准确和一致的?其实这个问题,也是G1里面,在并发标记阶段最难解决的问题。该问题不仅会发生在多次标记的场景下,也会发生在并发标记的过程中。

 

怎么保证并发标记的NextBitMap一定是正确的?怎么使用PrevBitMap来提升整体的标记效率?为了解决PrevBitMap和NextBitMap不一致的问题,G1引入了一个标记策略(三色标记法)和一个校对机制(SATB快照机制)来处理。

 

(4)总结

MixedGC并发标记算法包括:

一.初始标记阶段给Mixed GC带来了什么

二.对增量对象处理的一个简单思路

三.对象分配的特性—连续性

四.使用指针法来确定增量对象的范围

五.为什么要使用位图

六.与位图相关的指针

七.G1引入了的位图有哪些

八.进行第一次并发标记前

九.进行第一次并发标记

十.进行第二次并发标记

 

6.并发标记的三色标记法

(1)三色标记法中的白、灰、黑

一.白色

白色代表当前对象没有被访问过,如果在并发标记结束后某对象还是白色,就代表它是垃圾对象可以被回收。

 

二.灰色

灰色代表当前对象已被访问到,但Field字段没有被全部访问和标记完毕。即该对象是存活对象,但是其引用的子对象,还没有全都标记完成。

 

三.黑色

黑色代表当前对象已被访问到,并且其Field也已被全部访问和标记完毕。

 

当并发标记阶段结束时,所有的对象理论上要么是黑色、要么是白色,即对象要么是垃圾对象、要么是存活对象。

 

并发标记未结束的某个时刻下,对象的标记情况如下:

接下来详细演示整个三色标记法的标记过程。

 

(2)从GC Roots出发开始进行对象的标记

构造一个如下的场景:

对象是:
A、B、C、D、E、F、G;


引用关系为:
A.c = C; B.c = C; 
B.d = D; D.f = F; F.g = G;

其中的引用关系如下图示:

首先GC Roots直接引用的对象会被标为黑色,然后从GC Roots直接引用的这些对象出发,开始标记对象。

 

为什么被GC Roots直接引用的对象能直接标记为黑色?因为被GC Roots直接引用的对象是并发标记的起始点。这些对象是存活对象,最终肯定成为黑色的。

(3)并发标记访问到C和D,但C和D的Field还没被访问

此时C对象和D对象应该被标记为灰色。因为它们本身已经被标记,但是它们的Field还没有被扫描标记。而EFG对象因为此时还没有被访问到,所以还都是白色。

(4)没子对象的灰色对象标记为黑色,有子对象的继续访问

此时,C对象会被标记为黑色,因为它没有引用其他对象。D对象因为只有一个Feild为F,此时已经访问到F,那么F会被标记成灰色。当一个对象的所有子对象被标记成灰色时,对象本身就会被标记成黑色。

 

如下图示:此时因为D的所有子对象(F)已经被标记为灰色了,所以D被标记为黑色。而D的子对象F因为其Feild引用的对象G还没有被标记,所以此时对象F的状态为灰色。

(5)并发标记结束,所有被GC Roots引用链引用到的对象都被标记为黑色

接下来的过程和上面的流程类似,就是把所有可达对象都进行访问标记。这样就可以把所有存活对象都标记成黑色,而不可达对象就是保持白色。

ABCDFG对象的最终状态因为可达被标记黑色,E对象因为不可达而保持白色。在垃圾回收时,白色对象会被回收掉,黑色对象会继续存活在堆内存中,这就是三色标记法。

 

但是三色标记法,本身并没有解决错标漏标的问题,三色标记法仅仅描述对象在三种状态下的转变。那应该如何处理这些标记状态的转换,来保证标记的正确性?

 

(6)总结

并发标记的三色标记法:

一.三色标记法中的白、灰、黑

二.三色标记法的对象转换过程

经过并发标记的整个阶段结束以后,所有对象要么是黑色,要么是白色。黑色的对象是存活对象,白色的对象代表可以被回收。

 

7.三色标记法如何解决错标漏标问题

(1)标记出现漏标错标的条件

条件一:系统程序在并发标记阶段中,添加了一个黑色对象到某白色对象的引用

也就是在一个被标记为黑色的对象上,添加了一个到某白色对象的引用。因为黑色对象已经被标记了,如果不重新扫描一下这个黑色对象,那么这个白色对象将会被漏标,这样程序运行就会出现错误。当然,如果还有灰色对象在引用这个白色对象,其实也无所谓。因为灰色对象的字段会被遍历,重新找到这个白色对象进行标记,这样就不会出错。

 

条件二:系统程序在并发标记阶段中,删除了所有灰色对象到该白色对象的引用

 

仅仅发生条件一还可能不会出现错标漏标,但同时发生条件一 + 条件二,那么就必然会出现错标漏标。

 

场景演示如下:

一开始,E这个对象没有任何引用。此时并发标记已经开始一段时间了,系统也在不断地运行。

假如此时程序执行了一个操作:F.g = null; C.g = G; 那么此时的引用关系就会变成如下所示:

按照上面的思路分析:如果此时不对C进行重新处理,就会出现G被漏标的情况。因为C已经是黑色了,说明是存活对象,而且已经完成了标记。此时F正在标记过程中,但是F已经不再引用G了,那就会导致G被漏标,从而最终造成程序错误。

 

所以这个错标漏标问题必须要解决,而解决的思路之一就是:利用三色标记法的状态转换来保证最终标记的正确性。

 

(2)对象的标记信息存储在NextBitMap中

G1引入了NextBitMap来标记对象的状态,其实这些黑色、白色、灰色的状态就是存储在NextBitMap中。并发标记结束时,所有白色状态的对象都可以回收。

 

注意:NextBitMap记录了bottom指针到next指针范围内的内存使用的标记状态。

 

bottom指针:Region内存使用的起始地址

next指针:这一次并发标记开始前已经使用的内存结束地址

此时会有一个新的问题:并发标记阶段产生的新对象怎么标记、存储在哪里?

 

其实会在并发标记的下一个阶段(重新标记阶段)进行处理,重新标记阶段会把所有产生的新的对象进行重新标记,以及纠正在并发标记阶段造成的一些标记状态不正确的情况。

 

(3)三色标记法是怎么解决错标问题的

一.并发运行造成的错标漏标情景一

假如在并发标记过程中,已经完成了C对象的标记了。但是此时执行了一个C.e = E,那么此时E对象也是一个可达对象。但是并发标记过程却没有标记到,因为刚好错过了时间。并发标记标记完了C对象,然后去干其他事情了,而程序又执行了赋值操作。如下图示:

此时E对象已经可达了,但是却还是白色。如果该问题不解决就会导致E对象被回收掉,在使用C对象时出现系统错误。

 

二.并发运行造成的错标漏标的情景二

假如第一次并发标记已经标记到C、D对象,并且F已经被访问。此时CD都是黑色,然后并发标记被终止,于是只能进入下一次并发标记才能完成标记。此时的标记状态如下:

在下一次并发标记开始前,程序执行了D.e = E,此时E对象是可达的。但是在上一次被终止的并发标记中D对象已经被标记成黑色了。于是下一次并发标记开始后,E对象的白色状态不会改变,这样E对象在回收时就会被当作垃圾对象回收掉。

那么应该怎么解决错标漏标问题呢?

 

三.三色标记法解决错标漏标问题

当程序修改某对象的引用关系时,改变NextBitMap中该对象的标记状态。比如程序在执行了D.e = E时,则把NextBitMap中的对象D标记为灰色。然后在后续处理时,再把灰色的对象重新执行一遍即可。

当把所有的灰色对象都重新遍历一遍后,可能会产生一些新的垃圾对象。此时的最终状态如下,这样就能保证最终标记的正确性了。

(4)三色标记法如何解决错标漏标问题总结

一.标记出现漏标错标的条件

在黑色对象上添加一个到白色对象的引用,并且删除了所有灰色对象到该白色对象的引用。这可能发生在并发标记的程序运行时,也可能发生在并发标记被终止后。

二.对象的标记信息存储在NextBitMap中

三.三色标记法如何解决错标问题(增量更新)

把改变了引用的对象在NextBitMap中的标记重新置为灰色,然后在后续阶段再重新处理一下这个灰色对象的引用关系。

 

问题:除了三色标记法本身可解决错标漏标问题外,还有方法能解决该问题吗?SATB是怎么解决这个错标漏标问题的?

三色标记法本身解决 ---> 增量更新。

SATB解决 ---> 原始快照。

 

8.SATB如何解决错标漏标问题

(1)SATB的由来(PrevBitMap保存一份快照)

PrevBitMap主要用于记录从bottom指针到prev指针之间的内存区域里所有对象的标记状态。即上一次并发标记阶段被中断时标记到的所有对象的标记情况。

 

bottom指针:Region中内存使用的起始地址

prev指针:上一次并发标记后处理到的地址

 

PrevBitMap的第一个作用是:在并发标记开始时,PrevBitMap会被复制到NextBitMap来提升效率。

 

PrevBitMap的第二个作用是:由于PrevBitMap记录了上一次并发标记过程中的对象的标记情况,所以在PrevBitMap中可以知道已处理对象的标记状态。

 

那么本次并发标记开始前以及开始后,都有可能对已经标记好的那些对象做一些引用关系的改动,于是可以通过PrevBitMap知道这些对象在引用关系改变前的标记状态。

 

比如在第二次并发标记开始前,对象的标记状态如下:

这个其实就是SATB快照的由来。基于上一次并发标记标记出的PrevBitMap,就能知道对象的标记状态。根据PrevBitMap中记录的标记状态,就可以决定是否要对当前对象处理。

(2)SATB是如何处理变化的引用关系的

当执行了F.g = null; C.g = G操作时,对象的引用关系就会发生变化。

 

一.前面三色标记法自己的思路是增量更新

也就是把C这个对象先设置为灰色,然后重新从C出发,再来标记一次。

 

二.这里SATB方法的思路是原始快照

也就是把G这个对象先通过写屏障保存到一个地方。等到本轮次并发标记结束后,再把这些保存的没有遍历标记到的对象,再重新标记一次即可。此时会变成如下图示状态:

此时对象的引用关系已经变了,NextBitMap会自己继续处理自己的事,继续往后标记。

 

SATB这种做法,其实相当于把G这个对象置为灰色。而三色标记法,最终是需要把所有的对象标记成白色,或者是黑色的。所以灰色的对象是一定需要再重新处理一遍的,此时标记状态如下图:

当并发标记完成后,在重新标记阶段:就可以把这些在SATB队列里保存的对象拿出来进行可达性分析。

 

像CDF这些对象不需要重新标记,只需要把G是否可达分析一下即可。所以对SATB队列里的对象进行可达性分析,就能把全部对象正确标记上。最终整体的标记状态,就会是如下图示:

一.SATB队列已被清空

二.同时所有的存活对象、垃圾对象也都被标记在NextBitMap中

(3)SATB如何记录对象(把对象放入SATB队列)

G1会通过写屏障实现把对象放入SATB队列中。即在执行一些引用更新操作,把新对象赋值给老对象的某个字段前,会把这个新对象写入到SATB队列中。

 

可以将写屏障理解成一段增强代码,类似于AOP的思想,当然这种AOP思想是通过写屏障来实现的。当C.g = G被执行时,就代表引用关系可能会发生变化了。而当出现引用关系变化时,就会触发对象放入SATB队列这个操作。所以当C.g = G被执行时,对象G就会被写屏障识别并写入到SATB队列中。如下图示:

G1为了解决错标漏标问题,最终采用SATB方案来处理对象的重新标记。即:BitMap + 三色标记法 + SATB快照 + 写屏障 + SATB待处理对象队列,从而保证对象在标记过程的准确性。

 

此外,在处理RSet变更时也用到了写屏障,即用增强代码来写一条变更的消息到DCQ里面。

 

9.重新梳理Mixed GC的过程

(1)初始标记阶段STW

首先Mixed GC一定会伴随一次YGC。在YGC时开始时会做一个判断,看看是否需要开启并发标记。如果需要开启并发标记,那么在本次YGC的过程中,会额外做一些事情。比如把GC Roots直接引用的老年代对象也标记起来,从而这些被标记的老年代对象可以作为并发标记阶段的起始对象的一部分。

 

Mixed GC的初始标记阶段会发生YGC。YGC结束后,S区的对象会作为并发标记阶段的起始对象的一部分。

 

(2)并发标记阶段

YGC后会根据判定条件,开启并发标记线程,然后执行并发标记任务。然后从"S区存活对象 + GC Roots引用的老年代对象+ RSet"开始进行标记,并通过"位图NextBitMap + 三色标记法"来标记对象是否存活。然后通过SATB队列 + 写屏障把并发标记过程中的引用关系变化记下,从而解决错标漏标的问题。

 

在三色标记法中:白色代表垃圾、黑色代表存活、灰色代表存活但其子对象尚未完全遍历。

 

G1在做并发标记时,是不是也是按照分区来进行的?是的,G1本身就是从分区出发来处理的。所以如果要找某个特定分区的所有存活对象,需要借助RSet才能找到。

 

(3)最终标记阶段STW

最终标记阶段主要处理并发标记阶段由于系统运行造成的错标漏标问题。本质就是把SATB队列里的所有对象重新进行遍历标记处理。最终实现把全部对象都标记为黑色或者保持原本的白色。

 

最终标记阶段另外还会结束整个标记过程。因为在并发标记阶段,是不会进行STW的。所以如果不引入这个重新标记阶段,通过STW把所有对象都标记完成。那么并发标记阶段就会随着系统运行而持续不断地运行下去。

 

(4)预回收阶段

在最终标记阶段后,会进入预回收阶段,这个阶段会处理的事情如下:

 

一.根据RSet + NextBitMap完成存活对象计数并对Region排序

也就是统计每一个Region内部到底有多少对象存活、有多少垃圾对象。由于位图的存在,可以根据位图中的标记数据,把存活对象统计出来。完成存活对象的计数后,会根据统计结果对Region进行排序。

 

二.更新标记位图PreBitMap

更新PrevBitMap是为下一次并发标记做准备(如果回收成功则忽略)。因为虽然某个Region在本次并发标记进行标记了,但是未必会在混合回收中被选中,所以才需更新一下PreBitMap,以便Region在混合回收没选中时下一次标记还可用其标记过的位图。

 

三.重置RSet

此时老年代的分区已经完成标记。如果标记后的分区没有被对象引用,说明引用已经改变,此时就可以删除原来的RSet里的引用关系。

 

四.清理掉全部都是垃圾对象的分区,然后把分区放到空闲分区列表中

需要注意:这个阶段清理的分区只会针对所有对象都是垃圾对象的分区。对于存在存活对象的分区,不会有任何操作。所以很可能在这个预回收阶段结束后,JVM的内存使用情况没任何变化。

 

(5)混合回收阶段

在这个阶段,JVM需要选择一些分区进行回收。这些被选中的分区,就成为了CSet(Collect Set)。垃圾回收时会回收这些被选中的分区,然后把这些分区中的存活对象复制到空闲的分区。复制完成后便会清理掉这些被选中的分区,同时把清理完的分区放入到空闲列表中。

 

需要注意:在选择CSet时,是按照一定的选择算法来选择的。因为在上一个阶段已经统计出各分区的存活对象数量和垃圾对象数量,选择CSet时就会根据各分区的存活对象数量和垃圾对象数量进行选择。

 

(6)Mixed GC的整体流程图

10.选择CollectSet的算法是什么

(1)回收性价比是怎么计算的

G1在Mixed GC时会选择回收性价比最高的Region,那性价比会怎么算?

性价比 = 从该Region中可以回收掉的垃圾 / 回收该Region所需要的时间

比如一个Reigon总计32M,存活对象12M,垃圾对象20M。

复制12M存活对象 + 清空20M垃圾对象 + 清空12M已复制的对象 = 1ms

复制完12M对象到新Region后,可以回收掉的空间是12M + 20M=32M,那么这个Region的回收价值即性价比就是32M/ms。

 

因此对于一个Region来说:

垃圾对象越多,复制存活对象到新Region时间越少,回收性价比越高。

存活对象越多,复制存活对象到新Region时间越大,回收性价比越低。

 

(2)什么是回收时间

可回收垃圾的数量,可以通过位图NextBitMap来进行统计。因为位图标记了对象的存活状态,同时也描述了对象占用空间的大小。

 

那么回收时间应该如何确定呢?

首先回收时间更加准确的说法应该是:存活对象的转移时间。YGC和MixedGC都采取复制算法,把存活对象复制到一个空闲的新分区。然后再进行分区清理,把清理的分区加入到空闲分区列表中。所以真正耗时的,其实就是这个存活对象复制的过程。

 

因为对象复制涉及到了:对象本身和对象数据的复制、对象头更新、引用关系的指向更新、RSet和卡表等一系列的更新,所以对象复制这个过程是非常耗时的。

 

完成对象复制后,可以认为原来的Region里都是垃圾对象了。此时直接全部进行清空处理,基本不会耗费多少时间。

 

所以真正决定回收时间的,就是存活对象的转移时间。因此可以认为:垃圾回收需要的时间 = 存活对象的转移时间。

 

注意:对存活对象进行复制所消耗的时间其实占了GC绝大部分的时间。

 

(3)如何计算回收时间

根据前面介绍的停顿预测模型可知:在每次GC时,都会对整个GC耗费的时间,回收的垃圾数量进行统计,用来预测下一次GC时,应该选择多少分区,回收多少对象。

 

停顿预测模型里的预测内容和对象转移效率,基本上是同一个意思。根据停顿预测模型,预测出来的对象回收能力 = 单位时间回收多少对象,对象回收能力其实就是对象转移效率。

 

对象转移效率(单位时间转移多少对象/单位时间回收多少对象):如果一个Region中的存活对象越多或越大,那它需要的转移时间就越多。

Region的存活对象大小 / 单位时间转移多少对象 = 转移时间
Region的存活对象大小 / 单位时间回收多少对象 = 回收时间

如下图示有三个Region:第一个Region的存活对象只有一个,转移起来会比较容易,耗费的时间最短。第二个Region,转移起来会比较麻烦,耗费的时间最长。

对象转移的过程:首先需要把存活对象复制到一个空闲分区,然后执行一系列RSet更新、卡表更新、对象引用更新等操作,把原Region里的存活对象复制到一个空闲Region后,就可以清理掉原Region的所有对象了。

 

(4)停顿预测模型结合统计排序选择CollectSet

选择Collect Set的算法,其实就是结合一 + 二,就可以选择出性价比高的Region进行回收。

一.根据停顿预测模型计算出对象转移效率(单位时间转移多少对象)

二.在Mixed GC的预回收阶段得到Region垃圾对象和存活对象的统计排序

 

简单来说就是:

垃圾对象越多、存活对象越少的Region,回收时间就越少,性价比越高。

Collect Set的选择算法,就是选择垃圾对象越多的那些Region来回收。

 

(5)总结

对于这个问题,首先要说明的就是回收性价比。回收性价比 = Region中可回收的垃圾 / 回收一个Region所需要的时间。所以回收性价比就是单位时间内能回收多少垃圾,回收越多性价比越高。

 

注意:存活对象复制到新Region后,就相当于变成了垃圾对象。

 

Region中会有对象转移效率这个概念。对象转移效率可以看成是停顿预测模型中预测的回收能力,因此停顿预测模型预测的回收能力可代表Region的存活对象的转移效率。注意:停顿预测模型预测的回收能力是基于衰减标准差的。

 

那么Mixed GC的混合回收阶段在选择Region时,就可以基于并发标记阶段的标记内容,以及最终标记阶段对Region存活对象的统计,然后按照存活对象数量多少来选择Region,其中存活对象越少的Region就会越被选中。

 

假设Region大小4M,停顿预测模型计算出单位时间可以回收2M对象,也就是对象回收能力 = 2M/s,或者对象转移效率 = 2M/s:

第一个Region的存活对象1M,转移完存活对象、回收完1M需要时间 = 1M 除以 2M/s = 0.5s;
第二个Region的存活对象2M,转移完存活对象、回收完2M需要时间 = 2M 除以 2M/s = 1s;
第三个Region的存活对象3M,转移完存活对象、回收完3M需要时间 = 3M 除以 2M/s = 1.5s;
由于每个Region总共可以回收的垃圾 = 垃圾对象 + 存活对象(因为转移到另一个Region了) = 4M;
第一个Region的性价比,单位时间可以回收垃圾:4M 除以 0.5s = 8M/s;
第二个Region的性价比,单位时间可以回收垃圾:4M 除以 1s = 4M/s;
第三个Region的性价比,单位时间可以回收垃圾:4M 除以 1.5s = 2.7M/s;

11.Mixed GC的多次回收过程

(1)被选中的CSet

因为Mixed GC本身是包含了YGC的,所以Mixed GC在选择Region时,一定是包含了所有的新生代分区。虽然Mixed GC在初始标记阶段,已通过YGC完成了对新生代的回收。但本质上,一次Mixed GC混合回收选择的分区,还是包含了新生代所有分区以及老年代的部分分区。

 

另外还需注意:在Mixed GC混合回收阶段被选中的Region,不会出现里面全是垃圾,因为里面全是垃圾的Region在预回收阶段已经被清理掉了。

 

那么当全是垃圾的Region被清理掉后,剩下的Region到底选多少才合适?虽然G1会选择性价比高的Region,但性价比高的Region又应选择多少?

 

(2)加入CSet的Region的存活对象不能太多

-XX:G1MixedGCLiveThresholdPercent

这个参数决定了:是否可以把一个Region加入到CSet集合中。这个参数的含义是:一个Region的存活对象占这个Region的比例:如果大于或等于该参数值时,就不加入到CSet中。如果小于该参数值就可以加入到CSet中。

 

这个参数的默认值是85%。即如果存活对象大于或等于85%Region大小,那么这个Region就不值得回收,不能加入CSet。这时该Region的回收价值太低了,因为大量对象需要被复制到新的分区,特别耗时且腾出来的空间又很小。

 

(3)CSet可回收比例判断Mixed GC是否执行

-XX:G1HeapWastePercent

真正执行混合回收阶段对Region的回收,是在并发标记阶段完成后。而在真正执行混合回收阶段前,其实还需要做一层额外的判断。

 

注意:这个判断和是否开启并发标记的判断不是一个意思。这个判断是发生在并发标记之后,决定是否要开始进行回收。

 

这个判断由-XX:G1HeapWastePercent参数控制,默认5%。即在并发标记结束后:如果选择的CSet中可被回收的垃圾占堆内存总空间的比例大于5%,才会开始混合回收的回收阶段,否则本次是不开启垃圾回收过程的。即使并发标记阶段、最终标记阶段都已经完成,也不开启混合回收过程。

 

(4)MixedGC会对CSet进行分批次混合回收

-XX:G1MixedGCCountTarget

在判断通过之后,就会进入这最后一步的混合回收阶段。这个阶段因为要控制停顿时间,所以G1会分开多次执行。

 

具体几次是由一个参数控制:-XX:G1MixedGCCountTarget,默认为8。混合回收的过程会对CSet进行分批次回收,最多分8次来完成混合回收。

 

假如CSet总共有400个Region:每次为了满足停顿时间(假设100ms),发现只能回收50个,那么就会按照每次50个左右的Region来进行回收。因为按照Region的性价比排序,所以可能每次能回收的个数不一定是50。

 

当然,这个8次是不一定会完全进行到底的。当G1分批次回收时,会判断要不要进行下一次回收,判断的条件还是-XX:G1HeapWastePercent。也就是每执行一次混合回收,就会判断一下CSet里的垃圾对象占用的空间是否大于堆内存的5%。如果小于,就停止混合回收,本次回收就彻底结束;如果大于,则继续进行;

 

如果CSet里的Region非常多,每次选择的Region也非常多,这时该如何?

 

(5)分批次的混合回收选择的Region个数限制

-XX:G1OldCSetRegionThredShouldPercent,这个参数默认是10,表示每次在执行混合回收时:回收掉的分区数量不能超过整个堆内存分区数量的10%。

 

假如分了8次进行回收,结果发现某一次回收时,准备回收300个分区。而此时堆内存总共才2000个分区,那么此时是不会回收300个分区,而是只会回收200个分区。

 

(6)总结

1.加入CSet的Region存活对象占比不能太多——XX:G1MixedGCLiveThresholdPercent
2.混合回收是否要执行的判断条件之CSet可回收空间比例——XX:G1HeapWastePercent
3.MixedGC会对CSet进行分批次的混合回收——XX:G1MixedGCCountTarget
4.分批次的混合回收选择的Region个数限制——XX:G1OldCSetRegionThredShouldPercent

文章转载自: 东阳马生架构

原文链接: www.cnblogs.com/mjunz/p/186…

体验地址: www.jnpfsoft.com/?from=001YH