垃圾收集底层算法实现三色标记

三色标记
在并发标记的过程中 ,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。
三色标记:把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以 下三种颜色:
黑色 : 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过 灰色对象) 指向某个白色对象。
灰色 : 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
白色 : 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。

示例代码(与上面图无关,示例代码)
public class ThreeColorRemark {

    public static void main(String[] args) {
        A a = new A(); //忽略报错信息
        //开始做并发标记
        D d = a.b.d; // 1.读
        System.out.println("开始的d的内存地址"+d);
        if (Objects.isNull(d)){
            System.out.println("开始的d是null"+d);
        }
        a.b.d = null; // 2.写
        System.out.println("中间的d的内存地址"+a.b.d);
        if (Objects.isNull(a.b.d)){
            System.out.println("中间的d是null"+a.b.d);
        }
        a.d = d; // 3.写
        System.out.println("最后d的内存地址"+a.d);
        if (Objects.isNull(a.d)){
            System.out.println("最后的d是null"+a.d);
        }
    }
}
class A {
    B b = new B();
    D d = null;
}

class B {
    C c = new C();
    D d = new D();
}

class C {
}

class D {
}

讲一下代码运行与标记过程

1、首先CMS是有初始标记、并发标记、重新标记,其中初始标记、重新标记会STW。

相关信息可以看JVM垃圾收集器ParNew&CMS与底层三色标记算法详解_小丑竟是我自己-CSDN博客

2、当运代码执行到  A a = new A(); 

有了A的引用,对象A又引用了B,B引用了C、D

注意,在这一个时刻,Gcroots开始并发扫描,扫描到了A,并且扫描到了B,所以A变黑色,

扫描到了C,所以C变黑色,

但是还没有扫描到D,所以B是灰色,D是白色。

图应该是这样:(只是这一个时刻,并发扫描并没有结束)

 可以这么理解:黑色被分析完了、灰色还没有分析完、白色是还没有被分析过。

默认都是白色。(分析指的是:Gcroots可达性分析

3、在下一个时刻,b.d = null;  a.d = d;

 

 

------------------------------------------------------------------------------------------------------------------------------

多标-浮动垃圾

在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过
(被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“ 浮动
垃圾 ”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。
另外, 针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色 ,本轮不会进行清除。这部分
对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。
---------------------------------------------------------------------------------------------------------------------------------
漏标-读写屏障
漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,
有两种解决方案:
增量更新( Incremental Update)
原始快照( Snapshot At The Beginning,SATB)
增量更新 就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向 白色对象的引用之后, 它就变回灰色对象了
原始快照 就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后,
再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色( 目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾 )
人话版原始快照不会管新增的引用,它关心的是删除的引用,它会在集合里放一个指向D(对象)的引用,重新标记是,集合内所有引用全部标记为黑色。
以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过 写屏障 实现的。
写屏障
给某个对象的成员变量赋值时,其底层代码大概长这样: ( 以下皆为伪代码 )

所谓的写屏障,其实就是指在赋值操作前后,加入一些处理(可以参考AOP的概念):  

写屏障实现SATB
当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B 原来成员变量的引用 对象D记录下来:

 

写屏障实现增量更新
当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A 新的成员变量引用 对象D 记录下来:

 

 读屏障

读屏障是直接针对第一步:D d = a.b.d,当读取成员变量时,一律记录下来:

hotspot 的源码为(大致看下即可

可以看到,逻辑和展示的伪代码相似,源代码使用了队列,做了异步处理。

-----------------------------------------------------------------------------------------------------------------

现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色 集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可 以是广度/深度遍历等等。
对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:
CMS:写屏障 + 增量更新
G1,Shenandoah:写屏障 + SATB
ZGC:读屏障
工程实现中,读写屏障还有其他功能,比如写屏障可以用于记录跨代/区引用的变化,读屏障可以用于支持移动对象的并发执行等。功能之外,还有性能的考虑,所以对于选择哪种,每款垃圾回收器都有自己的想法。

为什么G1用SATB?CMS用增量更新?
我的理解:SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描
被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代
区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC
再深度扫描。
记忆集与卡表

猜你喜欢

转载自blog.csdn.net/b416055728/article/details/121499128