引言
在现代垃圾回收器中,并发标记 是实现低延迟目标的关键技术。而三色标记算法则是并发标记的理论模型和实现基础。它巧妙地通过三种颜色来模拟对象在标记过程中的状态,从而解决了在用户线程与垃圾收集线程并发执行时带来的复杂性问题。
一、核心思想:为何需要三色标记?
传统的"标记-清除"算法在标记时必须暂停所有用户线程,以防止对象引用关系在标记过程中发生变化。而三色标记法的核心目标,就是允许垃圾收集线程与用户线程并发执行,从而极大地减少垃圾收集时的停顿时间。
它将对象抽象为三种颜色,来表征其在标记过程中的状态:
1. 白色对象:
表示对象尚未被垃圾收集器访问过。在回收开始时,所有对象都是白色;在回收结束时,任何仍然是白色的对象,即被视为不可达,可以被回收。
2. 灰色对象:
表示对象已经被垃圾收集器访问过 ,但其至少还有一个引用的字段未被访问(即该对象直接引用的对象还没有被扫描)。灰色对象是"待扫描"的对象。
3. 黑色对象:
表示对象已经被垃圾收集器访问过 ,并且其所有直接引用的字段也都已经被访问过了。黑色对象是"已扫描"的对象,代表它是存活的,并且不会在此次收集中被再次扫描。
核心不变式 :任何黑色对象都不会直接指向白色对象 。这意味着,一旦一个对象被标记为黑色,它就不会再依赖任何白色对象(即不会被错误回收)。
二、工作流程:标记如何推进?
三色标记的过程可以看作是一个对象颜色从白到灰再到黑的演变过程。
初始阶段:
-
从GC Roots开始遍历。
-
所有GC Roots直接可达的对象被标记为灰色,并放入一个专门的"待扫描集合"。
-
其余所有对象均为白色 。
标记阶段:
-
**根对象标记:**垃圾回收器从GC Roots根对象(如栈中的引用、静态变量引用等)开始扫描,将根对象标记为灰色。
-
**灰色对象处理:**依次处理每个灰色对象,将其引用的所有白色对象标记为灰色,并将该灰色对象自身标记为黑色。
-
**循环处理:**重复上述步骤,直到所有灰色对象都变为黑色对象为止。
回收阶段:
-
标记阶段结束后,所有存活的对象都已经被标记为黑色。
-
所有剩余的白色对象即为不可达的垃圾对象,可以被安全地回收。
三、并发带来的挑战:漏标问题
在并发标记过程中,由于用户线程和垃圾回收线程同时运行,可能会出现以下两种情况,导致对象被错误地标记为垃圾:
- **条件一:**一个白色对象(未扫描的对象)被黑色对象(已扫描的对象)引用。
- **条件二:**从灰色对象到该白色对象(未扫描的对象)的直接或间接引用被破坏。

当这两个条件同时满足时, 由于黑色对象不会被再次扫描,而且灰色对象也无法引用这个白色对象,所以会导致白色对象被错误地标记为垃圾,这种现象称为"对象消失问题",也叫漏标。
四、解决方案:如何防止漏标?
1. 增量更新 - 破坏条件1
-
思想 :关注新增的引用。如果用户线程试图让一个黑色对象引用一个白色对象,JVM会通过写操作将这个新插入的引用记录下来。
-
行为 :在写操作中,会将这个新被引用的白色对象(C)直接标记为灰色,并重新放入"待扫描集合"。
-
效果:破坏了漏标的第一个条件。黑色对象A指向了白色对象C,但C立刻被变成了灰色,保证了它会被后续扫描到,从而不会漏掉。
-
代表收集器 :CMS 收集器在并发标记阶段就使用了增量更新算法。
2. 原始快照 - 破坏条件2
-
思想 :关注删除的引用 。在并发标记开始时,逻辑上为对象图创建一个快照。如果用户线程要删除一个灰色对象到白色对象的引用(即
B → C
),JVM会通过写屏障将这个即将被删除的引用关系记录下来。 -
行为 :在写屏障中,会将被删除引用指向的那个白色对象(C)标记为灰色。
-
效果 :破坏了漏标的第二个条件。即使删除了
B → C
,C也被变成了灰色,它仍然会被视为快照中的存活对象而被继续扫描。如果之后有新的引用指向它(如A → C
),它自然就是存活的。 -
代表收集器 :G1 收集器采用了原始快照算法。