【JVM】三色标记法
2026/6/7 12:15:43 网站建设 项目流程

三色标记法你可以理解成: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. 正常三色标记过程

假设引用关系是:

GC Roots -> A -> B -> C

一开始:

A、B、C 都是白色

初始标记阶段:

GC Roots 直接找到 A A 变成灰色

并发标记阶段:

扫描 A: A 引用了 B B 从白色变成灰色 A 扫描完成,变成黑色

继续扫描 B:

B 引用了 C C 从白色变成灰色 B 扫描完成,变成黑色

继续扫描 C:

C 没有引用其他对象 C 扫描完成,变成黑色

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

最终:

黑色对象:存活对象 白色对象:垃圾对象

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

初始标记:短暂 STW

初始标记只做一件事:

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

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

比如:

GC Roots -> A -> B -> C

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


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

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

过程是:

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

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

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


重新标记:再次短暂 STW

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

比如:

A 原来引用 B 用户线程突然让 A 不引用 B 了

或者:

A 原来不引用 B 用户线程突然让 A 引用 B 了

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

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


5. 漏标问题是什么?

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

所谓漏标就是:

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

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


6. 漏标发生的两个条件

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

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

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

例如:

A 是黑色对象 C 是白色对象

用户线程执行:

A.c=C;

于是变成:

黑色 A -> 白色 C

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

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


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

原本可能是:

灰色 B -> 白色 C

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

但是用户线程执行:

B.c=null;

变成:

灰色 B 白色 C

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


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

完整过程可以这样看:

原始引用关系:

GC Roots -> A -> B -> C

假设:

A 已经被扫描完,是黑色 B 还没扫描完,是灰色 C 还没被扫描,是白色

此时:

黑色 A -> 灰色 B -> 白色 C

用户线程做了两件事:

1. A 新增引用 C 2. B 删除引用 C

变成:

黑色 A -> C 黑色 A -> B B 不再引用 C

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

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

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

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

这就是漏标。


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

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

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

黑色 A -> 白色 C

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

灰色 B -> 白色 C

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

所以不会漏标。


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

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

B 不再引用 C

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

那 C 被回收是合理的。

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


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

CMS 解决漏标的思路是:

关注“新增引用”。

也就是你说的条件一。

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

A.c=C;

也就是:

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

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

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

可以理解成:

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

所以 CMS 的思路叫增量更新

“增量”指的是:

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


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

G1 解决漏标的思路是:

关注“删除引用”。

也就是你说的条件二。

原本:

灰色 B -> 白色 C

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

B.c=null;

G1 会通过写屏障记录下:

B 曾经引用过 C

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

它的核心思想是:

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

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

也就是说:

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

这样可以避免漏标。


10. CMS 和 G1 的区别总结

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

简单记法:

CMS:你新增了引用,我记下来。 G1:你删除了引用,我也记下来。

11. 多标问题是什么?

多标就是:

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

比如原来:

GC Roots -> A -> B

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

后来用户线程执行:

A.b=null;

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

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

所以 B 这次不会被回收。

这就是多标。


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

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

它不会造成程序错误。

这类对象也叫浮动垃圾

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

所以:

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

13. 最终可以这样记

三色标记法的核心逻辑:

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

并发标记的问题:

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

最严重的是漏标:

活对象没被标记,最后被错误回收

漏标发生条件:

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

解决方案:

CMS:增量更新,记录新增引用 G1:原始快照,记录删除引用

多标问题:

垃圾对象被当成存活对象,变成浮动垃圾 下次 GC 再回收

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询