三色标记法 (Three-Color Marking) 是 Go 实现并发 GC 的核心算法。它通过将对象分为黑、灰、白三种颜色,巧妙地实现了在程序运行的同时,准确找出不再使用的内存。
核心逻辑:三种颜色的身份牌
在标记过程中,每个对象都会被贴上一种颜色的标签:
| 颜色 | 状态描述 | 形象类比 |
|---|---|---|
| 白色 | 尚未被发现的对象 | 潜在垃圾:标记结束仍为白色则回收。 |
| 灰色 | 已发现但未处理完 | 待办事项:自己是活的,但还没看它引用的子对象。 |
| 黑色 | 已确认存活且处理完 | 安全对象:自己和子对象都已确认存活。 |
标记流程:从根出发的染色
graph LR Root((GC Roots)) --> A[对象A: 灰色] A --> B[对象B: 白色] A --> C[对象C: 白色] D[对象D: 白色] style Root fill:#f9f,stroke:#333 style A fill:#ccc,stroke:#333 style D stroke-dasharray: 5 5
- 初始状态:所有对象都是 白色。
- 根扫描:从 GC Roots 出发,将直接引用的对象染成 灰色。
- 并发标记:
- 从灰色队列取出一个对象,将其引用的子对象染成 灰色。
- 将该对象本身染成 黑色。
- 循环往复:直到灰色队列清空。
- 最终清理:回收所有仍然是 白色 的对象。
挑战:并发下的“漏标”风险
在并发标记阶段,业务代码(Mutator)可能会修改指针,导致 GC “看走眼”:
| 痛点场景 | 发生动作 | 结果 |
|---|---|---|
| 对象丢失 | 黑色对象指向了白色对象,且该白色对象的其他灰色引用断开了。 | 白色对象被误判为垃圾。 |
对策:三色不变性
为了解决漏标,Go 引入了 写屏障 (Write Barrier) 来维护以下任一约束:
- 强三色不变性:黑色对象绝对不允许指向白色对象。
- 弱三色不变性:黑色对象可以指向白色对象,但该白色对象必须有其他灰色对象在引用它(或其祖先)。
一句话总结:三色标记法通过“灰色”这个中间状态,将原本需要一次性完成的扫描工作变成了可以分步、并发执行的任务。