Go 专家(09):GC 三色标记深入 —— 并发标记、写屏障、调优
更新时间:2026-09-01。本文是
languages/go/expert/专家层第 09 篇,接 内存分配器。Go 的 GC 是并发三色标记清除(CMS),配合写屏障(write barrier)实现增量和并发。GC 调优就是调 GOGC 变量,控制 GC 频率和 CPU 开销。
本文要回答的问题
- 三色标记算法是什么?黑色、灰色、白色分别代表什么?
- 写屏障(write barrier)为什么需要?Go 用哪种屏障?
- GC 周期怎么划分?STW 多长?
- GOGC 调优怎么调?什么时候调大,什么时候调小?
一、三色标记算法
GC 标记阶段把对象分为三种颜色:
白色(待扫描)→ 灰色(待处理)→ 黑色(已完成)- 白色:初始状态,尚未被 GC 扫描到,这些对象可能是垃圾
- 灰色:已被扫描到,但它的引用对象还没扫描完
- 黑色:已被扫描完,它的所有引用对象都已处理
初始状态:所有对象都是白色
↓
从根(root)开始,把所有直接可达的对象标记为灰色
↓
每次取一个灰色对象,扫描它的引用:
- 被引用的对象:标记为灰色
- 当前对象:标记为黑色
↓
重复直到没有灰色对象
↓
剩下的白色对象就是不可达的 → 回收三色算法保证: 黑色对象不会引用白色对象。如果黑色对象引用了白色对象,白色对象会被 GC 当作垃圾回收掉——这就是写屏障的作用。
二、写屏障(Write Barrier)
GC 工作时,程序还在运行,可能会修改对象的引用关系。如果不加写屏障,可能出现:
黑色对象(已扫描完)→ 新引用了白色对象(未扫描)
GC 认为黑色对象已经完成了,不会再扫描它
→ 白色对象被当成垃圾回收
→ 程序崩溃(use-after-free)写屏障就是在 GC 期间,拦截所有指针写操作,保证:
黑色对象不能指向白色对象。
如果黑色对象要指向白色对象,写屏障把白色对象标记为灰色,三色不变性就保持了。
Go 1.14+ 使用 混合写屏障:
// 混合写屏障规则:
// 1. 灰对象引用白对象时,白对象标记为灰
// 2. 黑对象引用白对象时,白对象标记为灰
// 3. 删除引用时,被删除的对象标记为灰三、GC 周期
MARKING 1(标记阶段 1) ← 并发标记,和程序一起跑
├── GC STW(极小) ← 启动写屏障,时间很短
├── 并发标记 ← 主要工作,和程序并发
└── GC STW(极小) ← 关闭写屏障,时间很短
MARKING 2(标记阶段 2) ← 并发标记结束
↓
SWEEP(清扫阶段) ← 并发回收,和程序一起跑Go 1.19 的 GC 延迟通常在 1-3ms,大部分时间在 1ms 以下。
STW 时间:
- 开始标记:根对象扫描,~100μs
- 结束标记:~50μs
- 主要工作是并发完成的,STW 时间很短
四、GOGC 调优
// 默认值:100
// 含义:堆增长到上次 GC 后堆大小的 100% 时触发 GC
// 环境变量:GOGC=100
// 运行时修改:debug.SetGCPercent(100)GOGC 的含义:
GC 触发条件:当前堆大小 > 上次 GC 后堆大小 × (1 + GOGC/100)GOGC=100:堆大小翻倍时触发 GC(默认)GOGC=200:堆大小翻三倍时触发 GC,GC 频率更低,但峰值内存更高GOGC=50:堆大小增长 50% 时触发 GC,GC 频率更高,但峰值内存更低GOGC=off:关闭 GC(不推荐)
调优建议:
// 内存敏感、延迟敏感:降低 GOGC
GOGC=50 // 更频繁 GC,更多 CPU 开销,更低内存峰值
// 吞吐量敏感:提高 GOGC
GOGC=200 // 更少 GC,更低 CPU 开销,更高内存峰值五、GC 调优实践
目标: 找到 GC 频率和 CPU 开销的平衡点。
// 1. 用 pprof 看 GC 的 CPU 开销
// go tool pprof http://localhost:6060/debug/pprof/profile
// 2. 看 GC 频率
// runtime.ReadMemStats() 里的 NumGC 和 PauseTotalNs
// 3. 看 GC 的 CPU 占比
// 在 pprof 里看 runtime.gc 的调用栈时间调优场景:
- 大量临时对象,频繁分配 → 用 sync.Pool 复用,减少 GC 压力
- 大堆内存(> 10GB),GC 慢 → 调高 GOGC(如 200-500),减少 GC 频率
- 延迟敏感,不能容忍 GC 停顿 → 降低 GOGC(如 50-80),控制堆大小
六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| GOGC 调得太大 | 堆内存峰值高,可能导致 OOM | 根据内存限制调整 |
| GOGC 调得太小 | GC 频繁,CPU 浪费在 GC 上 | 根据 CPU 开销调整 |
| 全局变量大量引用 | GC 无法回收,内存泄漏 | 及时置 nil |
| 不关注 GC 的 CPU 开销 | 服务莫名变慢,以为是别的问题 | 先看 pprof 的 GC 占比 |
相关与延伸
下一篇:unsafe.Pointer 与 uintptr —— 内存地址操作、指针运算、go vet 检查;Go GC 的三色标记是经典算法,和 C 的内存管理对比,见 C malloc 底层。
一句话总结
Go GC 深入:三色标记算法——白(未扫描)、灰(待处理)、黑(已完成);写屏障保证并发标记的正确性,混合写屏障在 Go 1.14+ 使用;GC 周期中 STW 时间很短(~100μs),大部分工作是并发完成的;GOGC 默认 100,控制堆增长倍数再触发 GC;调低 GOGC 降低内存峰值但增加 CPU 开销,调高 GOGC 减少 GC 频率但增加内存峰值;pprof 看 GC 开销,sync.Pool 减少 GC 压力。