当前位置:网站首页>JVM 三色标记法与读写屏障
JVM 三色标记法与读写屏障
2022-07-26 00:12:00 【90后小伙追梦之路】
三色标记法
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
原始快照(STAB): G1,Shenandoah
总结了很多有关于java面试的资料,希望能够帮助正在学习java的小伙伴。由于资料过多不便发表文章,创作不易,望小伙伴们能够给我一些动力继续创建更好的java类学习资料文章,
请多多支持和关注小作,别忘了点赞+评论+转发。右上角私信我回复【999】即可领取免费学习资料谢谢啦!
边栏推荐
猜你喜欢

NVIDIA programmable reasoning accelerator tensorrt learning notes (III) -- Accelerating reasoning

Pikachu靶机通关和源码分析

MWEC:一种基于多语义词向量的中文新词发现方法

Sequence traversal II of leetcode107 binary tree

OPENCV学习DAY6

Nest. JS uses express but not completely

Bond network card mode configuration

Installation and configuration of VMware esxi7.0

FreeMarker view integration

07_ UE4 advanced_ MP value of firing fireball and mechanism of attacking blood deduction
随机推荐
京东按关键字搜索商品 API 的使用说明
What is software testing peer review?
【Redis】① Redis 的介绍、Redis 的安装
白蛋白纳米-超声微泡载组织型纤溶酶原激活物基因靶向制备研究
@RequestParam,@PathVariable两个注解的区别
Js理解之路:Js常见的6中继承方式
Understanding of "dbdnet: a deep boosting strategy for imagedenoising"
"Demons dance", is the bull market over? 2021-05-13
Binary tree -- 104. Maximum depth of binary tree
After using MQ message oriented middleware, I began to regret
Niuke / Luogu - [noip2003 popularization group] stack
牛血清蛋白修饰酚酸类及生物碱类小分子/偶联微球的蛋白/牛红细胞SOD的研究
Bond network card mode configuration
基于网络分析和文本挖掘的意见领袖影响力研究
Installation and configuration of VMware esxi7.0
【论文笔记】—目标姿态估计—EPro-PnP—2022-CVPR
Yes, UDP protocol can also be used to request DNS server
二进制表示--2的幂
Matlab makes the image of serial port output data in real time
Module II operation