G1 与三色标记法

921 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

1、G1的引入

CMS使用的是标记清除算法,所以不可避免地会产生内存碎片化的问题,虽然CMS提供了一个-XX:+UseCMSCompactAtFullCollection开关参数,用于CMS不得不进行FullGC时候开启内存碎片的合并整理,但是由于这个内存整理必须移动活动对象,所以无法并发。并且由于它本身的原理,他会产生浮动垃圾,随着内存的扩大,这个缺点会被不断放大,一旦老年代内存分配不下,就会默认启动SO,STW时间直线上升。所以就出现了G1,它吸取了CMS实现方法的教训,因为原本就是因为CMS导致内存碎片化问题,Serial Old处理太大内存导致STW时间延长,那么把内存分为一块块的region,将垃圾比较多的内存采用复制算法,这样效率极大地提升了。

2、原理

region分区

G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆空间划分为多个大小相等的Region,将region作为单次回收的最小单位,即每次收集到的内存空间都是Region大小的整数倍,每个Region都可以根据需要扮演Old、Survivor、Eden、Humongous(大对象区,有可能跨两个甚至跟多的region区,超过单个region50%,一般处理的时候当做老年代处理)。这里体现了分治的思想。

image.png

注意:

1、分区和分代矛盾吗?或者说G1分代吗?

首先,G1采取的是逻辑分代,物理不分代的模型。也就是说又分代有分区。G1将分区和分代结合了起来,这两者不矛盾,因为他们本质上都是为了避免一次性扫描较大堆内存空间。只是分代是根据对象的年龄大小进行划分,避免那些不需要扫描的区域进行频繁扫描。而分区是通过控制每次扫描的空间大小来减少每次扫描空间的大小。G1将两者结合了起来,首先,进行了分区,每次只要扫描几个region(由参数-XX:G1GeapRegionSize设定,大小范围1-32MB),避免扫描整个堆。而对象依然可以根据年龄大小划分为老年代和年轻代,所以不同的region扮演老年代,S区,E区和Uhmongous区,收集器能够对扮演不同角色的region采用不同的策略去处理,这样无论是新创建的对象还是已经存活一段时间的对象,还是经过多次收集的老年代对象来说,都能取得很好地回收效果

2、关于新生代和老年代比例

在5%-60%左右,不用手工指定,也不需要手工设定,这是G1预测停顿时间的基准,G1会动态调整。如果YGC本来是200ms,现在变成了400ms,那么下次就把Y区变小,这样动态的调整,以前的GC回收器都是手动调整Y区大小,得把机器停掉才行

3、G1如何实现可预测的停顿模型的?

其他GC都是只使用的分代模型,每次回收的时候都是以整个代为单位进行回收,回收的时间取决于相应代内存的大小以及垃圾的多少,所以停顿时间是不可预测的;而G1不是这样的,首先,也是所有所有的基础,它采用了基于Region的堆内存分布,region是单次回收的最小单位,通过衰减均值理论来决定现在开始回收的话,回收哪些region可以在不超过期望停顿时间的情况下获得最大的收益(-XX:MaxGCPauseMillis设置期望的停顿时间)

4、停顿时间和吞吐量如何进行权衡?

-XX:MaxGCPauseMillis设置停顿时间,但不是越小越好,因为停顿时间果断,就会造成每次清理的空间过小,进而导致G1必须进行多次回收,从而导致吞吐量降低,而吞吐量过低,会造成垃圾的堆积,最终导致FGC,而G1的目标就是不要有FGC,与调优背道而驰,所以需要根据实际压测的情况来进行取值,一般来说,就是几百毫秒的范围。从而权衡停顿时间和吞吐量

四个阶段

初始标记:找到根对象(此时是STW,但是根对象比较少,所以STW时间较少)

并发标记:工作线程运行,不断产生垃圾或者将垃圾变成资源。这个阶段比较容易出错(最耗时的阶段,但程序在运行,这时候不产生STW)

重新标记:并发标记阶段,垃圾有变化,进行重新标记,STW,但是时间也不长

并发回收:把垃圾全部清理

三色标记

为什么要用三色标记?

异步执行,传统的基于标记清除法,在GC起价,必须STW,不能异步执行GC操作,而STW对追求实时性的系统来说是不可接收的,三色标记法就是实现了异步操作,能在线程执行的过程中实现并发标记,从而极大地减少了STW时间

并发标记算法

把对象分成三种颜色;

1、黑色:自身和成员变量均标记完成

2、灰色:自己完成标记,成员变量没有完成标记

3、白色:均未发生标记

最开始所有对象都是白色,然后将自己完标记,但是成员变量没有完成标记的对象设置为灰色,随后将灰色对象的成员变量完成标记,变成黑色。当没有灰色对象的时候就把白色对象回收掉。

漏标问题:

并发标记过程中,指向白色对象的所有灰色对象没有再指向它,并且此时黑色对象指向它(黑色指向白色且指向白色的灰色没了)。此时出现漏标,这个白色对象不是垃圾,却会被当成垃圾被回收

解决办法:

从产生漏标的条件出发

1、增量更新(IU):跟踪黑色对象指向白色对象的增加,也就是把黑色对象重新标记为灰色(CMS使用的办法)

2、原始快照(STAB):跟踪灰色对象指向白色对象的消失,也就是灰色对象指向白色对象引用消失时,把这个引用保存下来,下次扫描还能接着扫描到(将这个引用推到堆栈中),保证白色对象还能被扫描到(GC里面有个栈,这个栈里面全是灰色对象指向白色对象的引用)

为什么G1使用STAB而不使用IU?

灰色->白色引用消失的时候,引用会被push到satb_mark_queue队列中,下次扫描的时候拿到这个引用,找到对应的白色对象,查看它所在的region的RSet,看有无对象指向它,而不需要扫描整个堆就能查找到指向白色对象的引用,效率比较高,注意,是SATB配合RSet效率比较高,不然光是SATB效率高的话,CMS为什么不用SATB而使用IU。

3、优缺点

优点:

1、并发收集

2、压缩空闲时间不会延长GC的暂停时间

3、能预测GC停顿时间

4、适合不需要高吞吐量,但需要快速响应时间的程序

5、由于G1整体上是基于标记整理算法,局部(两个region之间)是基于标记复制算法,不会存在内存碎片,有利于长时间运行

缺点:

内存占用和额外的执行负载高于CMS

参考

《深入理解Java虚拟机》

juejin.cn/post/692605…

juejin.cn/post/684490…

www.cnblogs.com/aspirant/p/…

tech.meituan.com/2016/09/23/…