【JVM】三色标记法

三色标记法你可以理解成:GC 在做"根可达性遍历"时,为了记录每个对象的扫描进度,把对象分成白、灰、黑三种状态。

它本质不是新的垃圾判断规则,真正判断对象是否存活的规则还是:

从 GC Roots 出发,能沿引用链找到的对象就是存活对象;找不到的就是垃圾对象。

三色标记法只是把这个"找"的过程变得更适合并发执行。


1. 为什么需要三色标记法?

假设没有并发标记,GC 要判断哪些对象活着,就需要:

  1. 暂停所有用户线程,也就是 STW;
  2. 从 GC Roots 开始遍历整个对象引用图;
  3. 遍历完之后,没被标记的对象就是垃圾。

问题是:堆越大、对象越多,遍历时间越久,STW 时间就越长。

所以 CMS、G1 这类低停顿垃圾回收器希望做到:

标记阶段尽量和用户线程一起执行,减少 STW 时间。

但是用户线程一边运行,一边修改对象引用关系,就会导致 GC 标记时出现"不一致"。三色标记法就是为了解决并发标记过程中的对象状态管理问题。


2. 三种颜色分别是什么意思?

可以这样理解:

白色:还没被 GC 访问过

白色对象有两种可能:

  1. 真的不可达,是垃圾;
  2. 只是还没来得及被扫描到。

所以在标记刚开始时,所有对象默认都是白色


灰色:对象本身被发现了,但它引用的对象还没扫描完

灰色对象代表:

这个对象已经确定是存活的,但它身上的引用还没有完全处理。

比如对象 A 引用了 B、C。

当 GC 找到 A 时,会先把 A 标成灰色,表示:

A 活着,但 A 里面引用的 B、C 还没看完。

灰色对象可以理解成"待处理队列"。


黑色:对象本身被发现了,并且它引用的对象也扫描完了

黑色对象代表:

这个对象确定存活,并且它直接引用的对象都已经被处理过了。

比如 GC 扫描完 A,发现 A 引用了 B、C,于是把 B、C 标成灰色,然后 A 自己变成黑色。


3. 正常三色标记过程

假设引用关系是:

text 复制代码
GC Roots -> A -> B -> C

一开始:

text 复制代码
A、B、C 都是白色

初始标记阶段:

text 复制代码
GC Roots 直接找到 A
A 变成灰色

并发标记阶段:

text 复制代码
扫描 A:
A 引用了 B
B 从白色变成灰色
A 扫描完成,变成黑色

继续扫描 B:

text 复制代码
B 引用了 C
C 从白色变成灰色
B 扫描完成,变成黑色

继续扫描 C:

text 复制代码
C 没有引用其他对象
C 扫描完成,变成黑色

最后没有灰色对象了,说明可达对象都扫描完了。

最终:

text 复制代码
黑色对象:存活对象
白色对象:垃圾对象

4. 初始标记、并发标记、重新标记分别干什么?

初始标记:短暂 STW

初始标记只做一件事:

找出 GC Roots 直接关联的对象。

它不会扫描整个堆,所以速度比较快。

比如:

text 复制代码
GC Roots -> A -> B -> C

初始标记只会先找到 A,把 A 标成灰色。


并发标记:和用户线程一起执行

并发标记就是从灰色对象开始,不断往下扫描引用链。

过程是:

text 复制代码
取出一个灰色对象
扫描它引用的对象
把它引用的白色对象变成灰色
自己扫描完后变成黑色

不断重复,直到没有灰色对象。

这个阶段不需要完全 STW,所以用户线程还在运行。


重新标记:再次短暂 STW

并发标记时,用户线程可能修改对象引用关系。

比如:

text 复制代码
A 原来引用 B
用户线程突然让 A 不引用 B 了

或者:

text 复制代码
A 原来不引用 B
用户线程突然让 A 引用 B 了

这些变化可能导致 GC 标记不准确。

所以重新标记阶段需要 STW,处理并发标记期间发生过变化的引用关系。


5. 漏标问题是什么?

漏标是三色标记法中最危险的问题。

所谓漏标就是:

一个对象明明还活着,但是 GC 没有标记到它,最终把它当垃圾回收了。

这会导致程序出错,因为用户线程后面可能还要用这个对象。


6. 漏标发生的两个条件

你总结的两个条件是对的。

漏标需要同时满足两个条件:

条件一:黑色对象新增了对白色对象的引用

例如:

text 复制代码
A 是黑色对象
C 是白色对象

用户线程执行:

java 复制代码
A.c = C;

于是变成:

text 复制代码
黑色 A -> 白色 C

问题是:A 已经被扫描完了,GC 后面不会再扫描 A。

所以 C 可能永远不会被发现。


条件二:灰色对象到白色对象的路径被切断

原本可能是:

text 复制代码
灰色 B -> 白色 C

正常情况下,GC 后面扫描 B 时,会发现 C,然后把 C 标成灰色。

但是用户线程执行:

java 复制代码
B.c = null;

变成:

text 复制代码
灰色 B   白色 C

这样 C 就不能通过灰色对象 B 被发现了。


两个条件同时满足才会漏标

完整过程可以这样看:

原始引用关系:

text 复制代码
GC Roots -> A -> B -> C

假设:

text 复制代码
A 已经被扫描完,是黑色
B 还没扫描完,是灰色
C 还没被扫描,是白色

此时:

text 复制代码
黑色 A -> 灰色 B -> 白色 C

用户线程做了两件事:

text 复制代码
1. A 新增引用 C
2. B 删除引用 C

变成:

text 复制代码
黑色 A -> C
黑色 A -> B
B 不再引用 C

此时 C 其实还是活着的,因为 A 引用了 C。

但是 GC 不会再扫描 A,因为 A 已经是黑色了。

B 也不再引用 C,所以扫描 B 时也找不到 C。

于是 C 一直是白色,最后被错误回收。

这就是漏标。


7. 为什么单独一个条件不会出问题?

只有条件一,不一定出问题

黑色对象 A 新增引用白色对象 C:

text 复制代码
黑色 A -> 白色 C

但如果灰色对象 B 仍然引用 C:

text 复制代码
灰色 B -> 白色 C

那么 GC 后面扫描 B 时,还是能找到 C。

所以不会漏标。


只有条件二,也不一定出问题

灰色对象 B 删除了对白色对象 C 的引用:

text 复制代码
B 不再引用 C

但是如果没有黑色对象新增引用 C,那么说明 C 可能真的不可达了。

那 C 被回收是合理的。

所以也不会造成"错误回收"。


8. CMS 的增量更新怎么解决?

CMS 解决漏标的思路是:

关注"新增引用"。

也就是你说的条件一。

当用户线程在并发标记期间执行:

java 复制代码
A.c = C;

也就是:

text 复制代码
黑色对象 A 新增引用白色对象 C

CMS 会通过写屏障把这个新增引用记录下来。

重新标记阶段,再处理这些记录。

可以理解成:

既然黑色对象 A 已经扫描过了,但它现在又新增了一个引用 C,那我重新标记时再补查一下 C。

所以 CMS 的思路叫 增量更新

"增量"指的是:

记录并发标记期间新增出来的引用关系。


9. G1 的原始快照怎么解决?

G1 解决漏标的思路是:

关注"删除引用"。

也就是你说的条件二。

原本:

text 复制代码
灰色 B -> 白色 C

用户线程把这个引用删掉:

java 复制代码
B.c = null;

G1 会通过写屏障记录下:

text 复制代码
B 曾经引用过 C

重新标记阶段,G1 会根据这些记录继续处理 C。

它的核心思想是:

按照并发标记开始那一刻的对象引用关系来判断对象是否存活。

所以叫 原始快照 SATB,Snapshot-At-The-Beginning。

也就是说:

只要对象在标记开始时是活的,即使后来引用被删了,G1 这次 GC 也先认为它是活的。

这样可以避免漏标。


10. CMS 和 G1 的区别总结

回收器 解决思路 关注哪个条件 核心思想
CMS 增量更新 黑色对象新增对白色对象的引用 新增的引用要补标
G1 原始快照 SATB 灰色对象删除对白色对象的引用 删除前的引用也要保留

简单记法:

text 复制代码
CMS:你新增了引用,我记下来。
G1:你删除了引用,我也记下来。

11. 多标问题是什么?

多标就是:

一个对象其实已经变成垃圾了,但这次 GC 仍然把它当成存活对象。

比如原来:

text 复制代码
GC Roots -> A -> B

GC 已经扫描到了 B,把 B 标成黑色。

后来用户线程执行:

java 复制代码
A.b = null;

此时 B 已经不可达了,理论上应该被回收。

但是 B 之前已经被标成黑色,GC 不会把它重新变回白色。

所以 B 这次不会被回收。

这就是多标。


12. 多标为什么可以接受?

因为多标只是让垃圾对象多活了一轮。

它不会造成程序错误。

这类对象也叫 浮动垃圾

本次 GC 没回收掉,下一次 GC 时,如果它仍然不可达,就会被回收。

所以:

text 复制代码
漏标:活对象被错杀,严重问题,必须解决。
多标:垃圾对象没回收,最多浪费一点内存,可以接受。

13. 最终可以这样记

三色标记法的核心逻辑:

text 复制代码
白色:还没访问,最终可能是垃圾
灰色:已经访问,但引用还没扫描完
黑色:已经访问,引用也扫描完

并发标记的问题:

text 复制代码
用户线程会修改引用关系,可能导致标记结果不准确

最严重的是漏标:

text 复制代码
活对象没被标记,最后被错误回收

漏标发生条件:

text 复制代码
1. 黑色对象新增对白色对象的引用
2. 灰色对象到白色对象的路径被切断

解决方案:

text 复制代码
CMS:增量更新,记录新增引用
G1:原始快照,记录删除引用

多标问题:

text 复制代码
垃圾对象被当成存活对象,变成浮动垃圾
下次 GC 再回收
相关推荐
wengad1 小时前
机器学习实践理论基础|算法、模型和数据集
人工智能·算法·机器学习
李白的天不白2 小时前
docker ps
java
NE_STOP2 小时前
Docker--Docker Swarm集群
java
两年半的个人练习生^_^2 小时前
JMM 进阶:彻底理解 CAS 实现原理
java·开发语言
wuminyu2 小时前
Java锁机制之park和unpark源码剖析
java·linux·c语言·jvm·c++
梦梦代码精2 小时前
为什么这个开源的AI平台会火?有点东西。。。
人工智能·算法·机器学习·docker·开源
随意起个昵称3 小时前
线性dp-综合刷题1(Not Alone)
算法·动态规划
W_LuYi1853 小时前
手撸极简zkEVM验证器:RISC-V电路实践
java·risc-v
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题 第102题】【并发篇】第2题:volatile 能否保证线程安全?
java·安全·面试