JVM系列6——垃圾收集底层算法实现-三色标记算法

文章详细介绍了JVM中解决对象存活判断问题的三色标记算法,包括初始标记、并发标记和重新标记三个阶段,以及在并发环境中为解决多标和漏标问题所采用的写屏障技术,如增量更新和原始快照。CMS和G1垃圾收集器分别采用了不同的策略,CMS使用增量更新,而G1采用原始快照方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        JVM判断对象是否存活的算法一般会采用可达性分析和引用计数法。引用计数法会存在循环引用的问题,比如两个对象相互引用,就形成了一个环形结构,一直无法被回收。可达性分析的过程STW时间长,在整个可达性分析阶段都需要STW,避免对象的状态发生变化,导致停顿时间长,影响了整体的性能。为了解决这两个算法的已知问题,引入了三色标记算法。

        三色标记算法是在并发标记过程中,因为标记期间应用县城还在继续跑,对象之间的引用可能会发生变化,多标和漏标的情况都会发生。三色标记是把GCroot可达性分析遍历对象过程中遇到的对象,按照是否访问过标记成三种颜色。

黑色:对象本身和所有的引用都被扫描过。是存活安全的,不能被回收。黑色不能不经过灰色直接指向白色。

灰色:对象本身被扫描过,至少还有一个引用没有被扫描。

白色:对象没有被扫描过。在可达性分析刚刚开始的阶段,所有对象都是白色的。扫描结束后对象还是白色就代表不可达可以被回收。

三色标记算法的标记过程分为三个阶段初始标记、并发标记、重新标记。

初始标记:遍历所有的根对象,将跟对象和直接引用的对象标记为灰色。在这个阶段垃圾回收器只会扫描直接或间接引用的对象,不会扫描整个堆。时间比较短。

并发标记:垃圾收集器从灰色对象开始遍历整个对象图,被引用的对象标记为灰色,已经便利过的对象标记黑色。并发标记过程中用户线程可能会修改图,需要使用写屏障保证并发标记的正确性。

重新标记:标记在并发标记中被修改的对象和未被遍历到的对象。垃圾回收器会重新开始遍历对象图,被引用的对象标记为灰色,已经遍历过的标记为黑色。需要stw。重新标记结束,垃圾回收器会执行清除的操作,不可达的白色对象将会被回收,释放内存。并发阶段比较耗时,因为需要遍历整个对象树,并且和应用线程并发执行,降低了GC的停顿时间。

多标-浮动垃圾:并发标记过程中,该对象还是有引用的,方法运行结束导致局部变量被销毁。导致它变成了一个垃圾对象。这个gcroor引用的对象被扫描过是黑色,这部分应该回收但是没有回收的内存,称为浮动垃圾。浮动垃圾不会影响垃圾回收的正确性,需要下一轮垃圾回收被回收掉。并非标记后新创建的对象直接标记成黑色,属于浮动垃圾的一部分。

漏标-读写屏障:漏标会导致被引用的对象被当成垃圾删除,漏标需要满足至少有一个黑色对象在自己被标记之后指向了这个白色对象和所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用。解决方案分为增量更新(破坏了第一个条件)和原始快照(破坏了第二个条件)。CMS采用的是增量更新方案,G1则采用的是原始快照的方案。

增量更新(Incremental Update):当黑色对象插入新的指像白色对象的引用关系时候,把新插入对象的引用记录下来,在重新标记阶段以这个黑色对象为根,对引用进行重新扫描。也就是说,黑色对象一旦新插入指向白色对象的引用后,就变回灰色对象了。

原始快照(Snapshot At The Beginning,SATB):如果灰色对象在扫描完成前删除对白色对象的引用,在取消引用之前,把灰色对象引用的白色对象记录下来。在重新标记阶段以记录的白色对象为根,对它的引用进行扫描,避免漏标的问题。通过这种方式让漏标的对象会被重新扫描成灰色,变成存活的状态。有可能产生浮动垃圾,需要等待下一轮GC的时候重新扫描。无论是增量更新的插入记录还是原始快照的删除记录,虚拟机的记录操作的都是通过写屏障实现的。

写屏障:在对象引用被修改时,将其新的引用信息记录在特殊数据结构中的机制。在三色标记算法中,写屏障技术被记录对象的标记状态,只对未被标记过的对象进行标记。应用程序修改一个对象的引用,写屏障会记录该对象的最新标记状态,如果没有标记,被标记为灰色。针对可达的对象写屏障不会进行任何操作。通过写屏障技术实现的三色标记算法能够确保垃圾收集器准备的标记所有的可达对象,并且避免对此标记同一个对象的情况。垃圾收集器通常会使用基于插入式的写屏障的优化技术,来降低写屏障的开销。

写屏障就是在赋值的前后处理操作:

/** 
* @param field 某对象的成员变量,如 a.b.d 
* @param new_value 新值,如 null 
*/ 
void oop_field_store(oop* field, oop new_value) { 
    pre_write_barrier(field); // 写屏障‐写前操作 
    *field = new_value; 
    post_write_barrier(field, value); // 写屏障‐写后操作 
} 

写屏障实现SATB就是当对象的成员变量饮用发生变化时,比如引用消失(a.b.d = null)利用写屏障,把b原来的引用对象d记录下来。

void pre_write_barrier(oop* field) { 
    oop old_value = *field; // 获取旧值 
    remark_set.add(old_value); // 记录原来的引用对象 
} 

写屏障实现增量更新:当对象A的成员变量引用发生变化,比如新增引用a.b=d,利用写屏障把A新的成员变量饮用对象d记录下来。

void post_write_barrier(oop* field, oop new_value) { 
    remark_set.add(new_value); // 记录新引用的对象 
} 

读屏障

oop oop_field_load(oop* field) { 
    pre_load_barrier(field); // 读屏障‐读取前操作3 return *field; 
} 
//读屏障D d = a.b.d,读到成员变量的时候记录下来。
void pre_load_barrier(oop* field) { 
    oop old_value = *field; 
    remark_set.add(old_value); // 记录读取到的对象 
} 

        CMS采用的是增量更新方案,G1则采用的是原始快照的方案的原因:原始快照相对增量更新效率会高,因为不需要在重新标记阶段再次深度扫描被删除的引用对象,而CMS的增量更新会做深度扫描,G1的对象都在不同的region,CMS只有一老年代区域,深度扫描G1的代价比CMS高,所以G1只是做简单的标记,有可能会产生浮动的垃圾,等到下一次GC在清理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值