GC 三色标记
问题
Go 的垃圾回收算法是什么?三色标记法如何工作?STW 停顿是多少?
答案
GC 发展历程
| Go 版本 | GC 算法 | STW 停顿 |
|---|---|---|
| Go 1.0 | 标记-清扫(STW) | 数百 ms |
| Go 1.3 | 并发标记 | 数十 ms |
| Go 1.5 | 三色标记 + 并发 | 数 ms |
| Go 1.8 | 三色标记 + 混合写屏障 | < 1ms |
| Go 1.12+ | 持续优化 | 通常 < 0.5ms |
三色标记法
将堆上所有对象分为三种颜色:
- 白色:未被访问,GC 结束后被回收
- 灰色:已被访问,但其引用的对象还未全部扫描
- 黑色:已被访问,其引用的对象也已全部扫描
标记过程:
- 初始化:所有对象标记为白色
- 从根出发:将根对象(全局变量、goroutine 栈)直接引用的对象标记为灰色
- 扫描灰色:取出一个灰色对象,将其引用的白色对象标记为灰色,然后将自身标记为黑色
- 重复步骤 3:直到没有灰色对象
- 清扫白色:剩余白色对象即为不可达对象,回收
并发标记的问题
GC 与用户代码并发执行时,可能出现漏标问题:
用户代码执行:黑色对象 A 新增引用白色对象 D
同时:灰色对象 B 删除了对 D 的引用
结果:D 已经不会被扫描到(B 不再引用 D,A 是黑色不会再扫描)
→ D 被错误回收!
写屏障(Write Barrier)解决这个问题——在指针赋值时通知 GC。
混合写屏障(Go 1.8+)
Go 1.8 采用混合写屏障(插入写屏障 + 删除写屏障的结合):
// 伪代码:每次指针写入时触发
func writePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
// 标记被覆盖的旧指针指向的对象(删除写屏障)
shade(*slot)
// 标记新写入的指针指向的对象(插入写屏障)
shade(ptr)
// 实际写入
*slot = ptr
}
混合写屏障的优势:
- 栈上操作不需要写屏障(栈对象会在 STW 时扫描)
- STW 只需要两次极短停顿(开始标记 + 结束标记)
GC 完整流程
两次 STW 总共 < 1ms,大部分标记和清扫工作与用户代码并发执行。
GC 触发条件
| 触发方式 | 条件 |
|---|---|
| 堆增长 | 堆大小增长到上次 GC 后的 1 + GOGC/100 倍 |
| 定时 | 2 分钟没有 GC 时自动触发 |
| 手动 | runtime.GC() 强制触发 |
常见面试问题
Q1: Go GC 的 STW 时间是多少?
答案:
Go 1.8+ 使用混合写屏障后,STW 只有两次极短停顿:
- Mark Setup(开启写屏障):通常 < 0.2ms
- Mark Termination(关闭写屏障):通常 < 0.1ms
- 总 STW 通常 < 0.5ms,与堆大小无关
标记和清扫阶段都是并发执行的。
Q2: 为什么 Go 不用分代 GC?
答案:
Java 的分代 GC 基于"大多数对象朝生夕灭"假设。Go 不用分代的原因:
- Go 的逃逸分析让短命对象留在栈上,堆上的对象本身就偏长命
- 分代 GC 需要写屏障追踪所有跨代指针引用(不只是 GC 期间),开销大
- Go 的并发三色标记 + 混合写屏障已经足够好
Q3: 三色标记的不变性是什么?
答案:
三色不变性(Tri-color Invariant):黑色对象不能直接引用白色对象。
维护这个不变性就不会漏标。写屏障的目的就是在并发修改指针时维护三色不变性。
Q4: 写屏障有什么性能开销?
答案:
写屏障只在 GC 标记阶段生效(通过全局标志位判断),未 GC 时无开销。GC 期间每次指针写入会多执行几条指令(检查标志 + shade),性能影响约 5~30%。Go 的混合写屏障不需要栈写屏障,减少了大量开销。
Q5: 如何观察 GC 行为?
答案:
# 方法 1:环境变量
GODEBUG=gctrace=1 ./myapp
# 输出格式
# gc 1 @0.012s 2%: 0.018+1.2+0.025 ms clock, 0.14+0.35/1.0/0+0.20 ms cpu, 4->4->2 MB, 5 MB goal, 8 P
# gc 序号 @程序启动时间 GC占CPU%: STW1+并发标记+STW2 clock时间, CPU时间, 堆变化, 目标堆大小, P数量
# 方法 2:runtime.ReadMemStats
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("GC 次数: %d\n", m.NumGC)
fmt.Printf("堆大小: %d MB\n", m.HeapAlloc/1024/1024)