G1垃圾回收器

G1设计的目标是让stop the world的时间是可预测和可配置的。C1垃圾回收器是软实时,低延迟的垃圾回收器,你可以设置你需要的性能目标。它解决了CMS中空间碎片的问题,同时因为G1在低延迟,高吞吐量方面都比较好,所以在JDK9取代了默认的Parallel GC 关注吞吐量的组合成为默认的垃圾回收器,而CMS在Jdk9被移除了。


G1不要求将年轻代和老年代用连续的空间来实现,堆会被划分成一系列的小resion,通常默认是2048块,任何一个region可能是eden区,survivor区,或者是老年代区,但是不能一个region有多个区,只能是其中一个。每块Region取值范围为1M到32M,并且只能是2的n次幂,可以通过虚拟机参数:

-XX:G1HeapRegionSize来设置大小,所有的eden和survivor区的总和就是年轻代的大小,所有老年代的总和就是老年代的大小。如下图:

这种化分区域的方式,使得垃圾回收的时候,不需要对很大的一块区域进行垃圾回收(比如serial系列垃圾收集器,要么整块年轻代,要么整个堆进行回收),而是逐步的进行回收,仅仅回收一部分region,即回收region的整数倍区域。年轻代回收的时候,会stop the world,当存在跨代引用的时候会包含部分的老年代被回收,这一次回收的部分称为回收集。如下图,被框了(有黑色框)的部分就是这一次垃圾回收会处理的region。

G1可以评估每一个region存活对象的数量和回收该区域所需要的时间经验值,来维护一个优先级列表,每次根据用户设置的停顿时间(可以使用参数:-XX:MaxGCPauseMillis设置),优先回收包含比较多垃圾对象的regions,这也是该垃圾回收器名称Garbage first的由来。


可以使用JVM参数 -XX:+UserG1GC来启用G1垃圾回收器。随着服务启动,处理请求,年轻代空间被使用完,就会stop the world,进行垃圾回收处理,将所有存活的对象,拷贝到survivor区,这个处理阶段被称为疏散阶段,这个阶段和其他垃圾回收器,比如serial,Parnew等非常相似。除了上述的年轻代,老年代,survivor区域外,G1中还有一种特殊的区域,humongous,用来存储大对象用的,当需要分配一个连续超过region一半的连续大对象的时候,那么就会使用Humongous这块区域,这块区域的会被当作老年代来处理。


卡表:将整个堆划分为大小512KB的小内存,被实现为一个简单的字节数组byte[],即卡表的每个标记项为1个字节。有相应的索引值index,0,1,2...., 那么对应的地址就为0,512,1024...


跨代引用或者混合GC中老年代跨region引用的处理:

使用remembered set避免全堆扫描,每一个region都维护一个remembered set,这些结构会记录别的regionA指向该regionB的引用地址信息,并记录regionA的卡表信息,就regionA中哪个卡表索引区域的对象引用了regionB的对象,标记为dirty。remembered set本质上是一个hash结构,key存的时候region的起始地址,value是卡表索引号集合(被标记dirty的卡表索引)。这些额外的存储内存,大约要损耗堆内存的10%到20%的大小,额外执行的负载也相对比较高。

如上图.我们可以看到三块灰色的region: Region 1, Region 2 and Region 3 和他们相关的粉红色的 RSets, Region 1 和Region 3 引用Region 2的对象.。因此region2可以回溯找到region1和region3的卡表索引。

写屏障

每次reference类型数据的写操作,都会产生一个write barrier 中断操作,然后检查将要写入引用的对象是否和该reference类型数据在不同region,如果不同,那么把相关引用信息,记录到引用对象的region对应的remembered set中。当进行垃圾回收的时候,在gc roots集中加入region的remembered set,就可以保证就算不全局扫描,也不会有遗漏。

对于引用类型的复制语句,比如 person.t = teacher,假设新生代对象: person是分配在region A,然后老年代对象teacher分配在region B。那么此时就存在跨代引用。在进行赋值之前会产生write barrier ,会写入到dirty card queue中,当这个queue达到一定值或者时年轻代要执行垃圾回收时,对card queue中的卡表索引进行处理,进行更新remembered set保证remembered set实时准确。

这里网上很多资料都说引入了卡表(byte[]数组,CMS中有用到)或者Rset(在卡表结构上实现的一种hash结构,G1采用的一种结构)结构,在进行young gc解决跨代引用问题时,可以避免对整个堆进行扫描。其实我们知道并没有哪一种垃圾回收器会扫描整个堆,都只是从GC Roots集合中出发,标记存活对象而已。


回收步骤:

1、初始标记:仅仅是比较一下GC Roots能直接关联的对象,并且修改

TAMS指针的值,让下一阶段用户线程并发运行的时候,能正确在可用的

region中分配新对象。这个阶段需要stop the world,但是停顿很小。


2、并发标记:从GC Root出发,扫描堆,找到要回收的对象,这个阶段耗时比较长,但是可以和用户程序并发执行。当对象扫描完成以后,还要重新处理SATB记录下的并发时有引用变化的对象。


3、最终标记:对用户线程做另外一个短暂的暂停,用于处理并发阶段结束后能遗留下来的少量SATB记录。

4、筛选回收:负责更新region的统计数据。

三色标记算法:

三色标记算法,是将对象分成三种类型,黑色,灰色和白色。

黑色:根对象,或者是该对象被标记过了,同时它的所有属性也被标记完了。

灰色:本身对象被标记了,但是属性还有没被标记的。

白色: 未被标记的,当所有的扫描都完成后,白色的对象就是不可达的对象,也就是垃圾对象。

并发标记过程三色标记算法:1、开始时,将根节点的子对象标记为灰色。

2、从灰色开始遍历,将已经遍历了子对象的节点标记为黑色。
3、当所有的标记过程结束后,如下图,白色的对象即是不可达的垃圾对象。
因为标记过程是和程序并发执行的,可能会出现对象漏标问题。例如,对象A已经被置为黑色,对象B被置为灰色,同时对象B引用着白色的对象C。因为标记过程是和运用程序并发执行的,所以有可能出现对象B将对象C的引用断开,同时对象A又引用对象C或者对象D的情况。那么继续标记的话,就会出现漏标对象C或者对象D的情况,这个问题使用SATB算法解决。

SATB算法:在开始标记的时候,会生成一个存活对象的快照,在并发标记的过程中,所有引用改变的对象,都入队(在产生写屏障的时候,将旧引用指向的对象置为非白的)这种方式可能会存在浮动垃圾(内存泄漏),将在下一次进行回收。

参考文献:https://plumbr.io/handbook/garbage-collection-algorithms-implementations#g1https://www.infoq.com/articles/tuning-tips-G1-GC/https://tech.meituan.com/2016/09/23/g1.html


你可能感兴趣的:(G1垃圾回收器)