go的三色标记法并非教科书式实现,而是与混合写屏障等机制深度耦合的工程方案;对象颜色不暴露、不存于对象头,仅由位图和队列逻辑维护,且写屏障可使对象跳过灰色直接变黑。

Go 的垃圾回收器(GC)从 1.5 版本起就采用三色标记法(Tri-color Marking),但它不是教科书式的纯算法实现,而是与写屏障、混合写屏障(hybrid write barrier)、辅助 GC(mutator assist)等机制深度耦合的工程方案。直接照搬论文理解 Go 的三色标记,反而容易误解实际行为。
为什么 runtime.GC() 触发后看不到“标准三色状态”
Go 运行时不会暴露对象的颜色字段(white/grey/black),也不提供 API 查询某个对象当前颜色。所谓“三色”,是 GC 在标记阶段内部维护的状态抽象,仅存在于标记工作队列和内存页元信息中。
- 标记开始时,所有对象默认为
white(未访问) -
grey对象只存在于标记队列(gcWork结构)或被写屏障临时标记的栈/堆对象中 -
black并非“已扫描完成”,而是“该对象及其子对象都已确认可达”,但 Go 会延迟将对象置 black——尤其在开启混合写屏障后,很多对象会跳过grey直接从white变为black - 你用
unsafe去读mheap.arena或mspan.spanClass也拿不到颜色,因为颜色不存于对象头,而由位图(gcBits)和辅助结构联合编码
写屏障如何让 Go 的三色标记“不守规则”
经典三色标记要求:不能存在从 black 到 white 的新引用(否则漏标)。Go 1.8+ 使用混合写屏障(hybrid write barrier),其行为是:任何写操作,只要旧值非 nil,就将旧对象标记为 grey;同时,新值所在对象会被立即标记为 black(即使它还没被扫描)。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
- 这意味着:一个刚分配的对象(
white)被赋值给某 black 对象字段时,它会立刻变black,跳过grey队列 - 所以你观察
runtime.ReadMemStats中的NextGC和HeapLive,会发现标记过程“跳跃式”推进,不像教科书里逐层扩散 - 这也解释了为什么 Go GC 能容忍并发赋值:写屏障把“破坏三色不变性”的风险,转为对旧对象的保守重标记(marking the old object grey)
怎么验证三色标记正在运行(而不是猜)
唯一可观察的信号来自运行时调试接口和 GC trace,而非代码逻辑:
- 设置
GODEBUG=gctrace=1,启动程序后看到类似gc 2 @0.021s 0%: 0.017+0.49+0.021 ms clock, 0.14+0.060/0.37/0.29+0.17 ms cpu, 4->4->2 MB, 5 MB goal, 8 P的日志,其中0.017+0.49+0.021三段分别对应 STW mark setup、并发标记、STW mark termination,这就是三色标记的三个关键阶段 - 用
go tool trace打开 trace 文件,在 “Goroutines” 视图中找GC worker和STW事件,标记阶段的 goroutine 会大量调用scanobject和shade(后者即写屏障触发的着色函数) - 注意:
runtime/debug.SetGCPercent(-1)关闭 GC 后,三色标记完全不启动;但runtime.GC()是强制触发一次完整循环,包括三色标记 —— 它不是“手动执行标记”,而是请求调度器安排一次 GC cycle
真正容易被忽略的是:Go 的三色标记不是独立模块,它和栈重扫(stack rescan)、对象分配路径、span 分配策略、甚至 defer 链表管理都交织在一起。想靠“学懂三色”来调优 GC,不如先看 gctrace 输出里的 assist time 和 idle GC 比例——那些才是影响你服务延迟的实际变量。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










