go三色标记法是运行时协同推演的可达性分析协议,状态由gcwork队列和mbitmap位图动态维护,非对象字段存储;混合写屏障必须shade(old)和shade(ptr)以防止漏标,stw仅限根标记和标记终止两次。

Go 的三色标记法不是给对象“染色”,也不是靠对象字段存颜色状态;它是一套运行时协同推演的可达性分析协议,依赖 gcWork 队列、mbitmap 位图和写屏障共同维持强三色不变式——漏掉任一环节,就会漏标或误回收。
三色状态到底在哪儿?不在对象内存里
很多人调试时打印 obj.Color,以为这是 GC 实际使用的颜色。错。Go 运行时中根本不存在对象结构体里带 Color 字段的设计(除了教学 demo)。真实三色状态由两部分动态维护:
-
mbitmap:每个堆对象对应一个 bit,只记录“是否已扫描”(本质是黑白二值) -
gcWork队列:灰色对象实际就是正在被 worker 处理或待处理的指针地址,队列为空即无灰色
白色 = 未被任何路径访问过(不等于垃圾);灰色 = 已入队但子引用未扫完;黑色 = 已出队且所有子引用确认完成。这些状态不固化、不持久,纯属当前 GC 周期的瞬时视图。
混合写屏障为什么必须同时 shade(old) 和 shade(ptr)
并发场景下最危险的竞态是:黑色对象 A 的字段从指向白色 C 改为指向白色 B,而 GC 已扫完 A —— B 就永远进不了灰色队列,最终被清掉(漏标)。混合写屏障在每次 *slot = ptr 时插入,仅对堆上指针写生效(结构体字段、map value、切片元素),伪逻辑如下:
if gcphase == _GCmark {
if old != nil { shade(old) } // 保旧对象不死(C 可能还被其他路径引用)
if ptr != nil { shade(ptr) } // 拉新对象入队(B 必须被后续扫描到)
}
注意:shade 不是改对象颜色,而是把 old 和 ptr 对应的堆地址压入 gcWork 队列。栈上赋值、int/string 底层数据、unsafe.Pointer 绕过类型系统,都不触发该屏障。
STW 真实发生在哪里,为什么只有两次
很多人看 go tool trace 里 GC pause 很短,就以为“没停顿”。其实 STW 是硬性必需,只发生在两个原子切片:
-
root marking:暂停所有 goroutine,遍历所有栈帧 + 全局变量,把根对象标灰(必须原子,否则栈在变) -
mark termination:再次 STW,清空残留灰色队列 + **重扫所有 goroutine 栈**(因栈小、变更快,重扫比插屏障更高效)
中间的并发标记阶段完全不停应用,但写屏障全程开启;清理阶段也并发执行。所谓“百微秒级 STW”,指的就是这两次暂停的总和,而非单次时长。
新分配对象默认为白色,哪怕刚被栈变量引用
这是最容易踩坑的点:你写 obj := &MyStruct{...},这个 obj 在堆上分配后初始状态就是白色 —— 即使它立刻被栈上局部变量持有。关键在于:
- 栈上变量在 root marking 阶段被一次性扫描并标灰(之后不再重扫栈)
- 所以新对象能否活下来,取决于它是否在 root marking 完成前被栈变量引用并进入灰色队列
- 如果分配发生在 root marking 之后、写屏障启用期间,则必须靠写屏障把它拉进队列,否则就是白色→被清
这也是为什么逃逸分析结果直接影响 GC 压力:逃逸到堆的对象越多,写屏障触发越频繁,灰色队列越容易积压,标记延迟越高。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











