JAVA面试题分享五十九:JVM 中的三色标记法是什么?
GC 垃圾回收器其主要的目的是为了实现内存的回收,在这个过程中主要的两个步骤就是:内存标记,内存回收。三色标记法,主要是为了高效的标记可被回收的内存块。白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代 表已经扫描过,它是
目录
一、前言
GC 垃圾回收器其主要的目的是为了实现内存的回收,在这个过程中主要的两个步骤就是:内存标记,内存回收。
二、三色标记法简介
三色标记法,主要是为了高效的标记可被回收的内存块。
三色标记(Tri-color Marking)作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:
- 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代 表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对 象不可能直接(不经过灰色对象)指向某个白色对象。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
三、三色标记过程
标记过程:
- 在
GC
标记开始的时候,所有的对象均为白色; - 在将所有的
GC Roots
直接引用的对象标记为灰色集合; - 如果判断灰色集合中的对象不存在子引用,则将其放入黑色集合,若存在子引用对象,则将其所有的子引用对象存放到灰色集合,当前对象放入黑色集合。
- 按照此步骤 3 ,依此类推,直至灰色集合中所有的对象变黑后,本轮标记完成,并且在白色集合内的对象称为不可达对象,即垃圾对象。
- 标记结束后,为白色的对象为 GC Roots 不可达,可以进行垃圾回收。
四、误标
三色标记的过程中,标记线程和用户线程是并发执行的,那么就有可能在我们标记过程中,用户线程修改了引用关系,把原本应该回收的对象错误标记成了存活。(简单来说就是 GC
已经标黑的对象,在并发过程中用户线程引用链断掉,导致实际应该是垃圾的白色对象但却依旧是黑的,也就是浮动垃圾)。这时产生的垃圾怎么办呢?答案是本次不处理,留给下次垃圾回收处理。
而误标问题,意思就是把本来应该存活的垃圾,标记为了死亡。这就会导致非常严重的错误。那么这类垃圾是怎么产生的呢?
途中对象 A
被标记为了黑色,此时它所引用的两个对象 B
,C
都在被标记的灰色阶段。此时用户线程把B->D
之间的的引用关系删除,并且在A->D
之间建立引用。此时B
对象依然未扫描结束,而A对象又已经被扫描过了,不会继续接着往下扫描了。因此 D
对象就会被当做垃圾回收掉。
什么是误标?当下面两个条件同时满足,会产生误标:
- 赋值器插入了一条或者多条黑色对象到白色对象的引用
- 赋值器删除了全部从灰色对象到白色对象的直接引用或者间接引用
误标的解决方案
要解决误标的问题,只需要破坏这两个条件中的任意一种即可,分别有两种解决方案:增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning, STAB)
增量更新
增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
原始快照 (STAB)
原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
五、漏标和多标
对于错标其实细分出来会有两种情况,分别是:漏标和多标
多标-浮动垃圾
如果标记执行到 E 此刻执行了 object.E = null
在这个时候, E/F/G 理论上是可以被回收的。但是由于 E 已经变为了灰色了,那么它就会继续执行下去。最终的结果就是不会将他们标记为垃圾对象,在本轮标记中存活。
在本轮应该被回收的垃圾没有被回收,这部分被称为“浮动垃圾”。浮动垃圾并不会影响程序的正确性,这些“垃圾”只有在下次垃圾回收触发的时候被清理。
还有在,标记过程中产生的新对象,默认被标记为黑色,但是可能在标记过程中变为“垃圾”。这也算是浮动垃圾的一部分。
漏标-读写屏障
写屏障(Store Barrier)
给某个对象的成员变量赋值时,其底层代码大概长这样:
/**
* @param field 某个对象的成员属性
* @param new_value 新值,如:null
*/
void oop_field_store(oop* field, oop new_value) {
*fieild = new_value // 赋值操作
}
所谓写屏障,其实就是在赋值操作前后,加入一些处理的逻辑(类似 AOP 的方式)
void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field);
// 写屏障-写前屏障 *fieild = new_value
// 赋值操作 pre_write_barrier(field);
// 写屏障-写后屏障
}
写屏障 + SATB
当对象E的成员变量的引用发生变化时(objE.fieldG = null;),我们可以利用写屏障,将E原来成员变量的引用对象G记录下来:
void pre_write_barrier(oop* field) {
oop old_value = *field; // 获取旧值
remark_set.add(old_value); // 记录 原来的引用对象
}
【当原来成员变量的引用发生变化之前,记录下原来的引用对象】 这种做法的思路是:尝试保留开始时的对象图,即原始快照(Snapshot At The Beginning,SATB) ,当某个时刻 的GC Roots确定后,当时的对象图就已经确定了。 比如 当时 D是引用着G的,那后续的标记也应该是按照这个时刻的对象图走(D引用着G)。如果期间发生变化,则可以记录起来,保证标记依然按照原本的视图来。
值得一提的是,扫描所有GC Roots 这个操作(即初始标记)通常是需要STW的,否则有可能永远都扫不完,因为并发期间可能增加新的GC Roots。
SATB破坏了条件一:【灰色对象 断开了 白色对象的引用】,从而保证了不会漏标。
一点小优化:如果不是处于垃圾回收的并发标记阶段,或者已经被标记过了,其实是没必要再记录了,所以可以加个简单的判断:
void pre_write_barrier(oop* field) {
// 处于GC并发标记阶段 且 该对象没有被标记(访问)过
if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
oop old_value = *field; // 获取旧值
remark_set.add(old_value); // 记录 原来的引用对象
}
}
写屏障 + 增量更新
当对象D的成员变量的引用发生变化时(objD.fieldG = G;),我们可以利用写屏障,将D新的成员变量引用对象G记录下来:
void post_write_barrier(oop* field, oop new_value) {
if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
remark_set.add(new_value); // 记录新引用的对象
}
}
【当有新引用插入进来时,记录下新的引用对象】
这种做法的思路是:不要求保留原始快照,而是针对新增的引用,将其记录下来等待遍历,即增量更新(Incremental Update)。
增量更新破坏了条件二:【黑色对象 重新引用了 该白色对象】,从而保证了不会漏标。
读屏障(Load Barrier)
oop oop_field_load(oop* field) {
pre_load_barrier(field); // 读屏障-读取前操作
return *field;
}
读屏障直接针对第一步 var objF = object.fieldG;
,
void pre_load_barrier(oop* field, oop old_value) {
if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
oop old_value = *field;
remark_set.add(old_value); // 记录读取到的对象
}
}
这种做法是保守的,但也是安全的。因为条件二中【黑色对象 重新引用了 该白色对象】,重新引用的前提是:得获取到该白色对象,此时已经读屏障就发挥作用了。
六、CMS 漏标解决方案
CMS 回收器采用的是增量更新方案,即破坏第一个条件:「有至少一个黑色对象在自己被标记之后指向了这个白色对象」。
既然有黑色对象在自己标记后,又重新指向了白色对象。那么我就把这个黑色对象的引用记录下来,在后续「重新标记」阶段再以这个黑色对象为跟,对其引用进行重新扫描。通过这种方式,被黑色对象引用的白色对象就会变成灰色,从而变为存活状态。
这种方式有个缺点,就是会重新扫描新增的这部分黑色对象,会浪费多一些时间。但是这段时间相对于并发标记整个链路的扫描,还是小巫见大巫,毕竟真正发生引用变化的黑色对象是比较少的。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)