Go GC 垃圾回收机制
Go GC 目标是低延迟:STW 控制在毫秒级内。
演进历史
| 版本 | 算法 | STW 时间 |
|---|---|---|
| 1.3 之前 | 标记-清扫(STW) | 几百毫秒到秒级 |
| 1.5 | 三色标记 + 并发清扫 | 大幅缩短 |
| 1.8 | 混合写屏障 | 亚毫秒级 |
当前 Go runtime 的 GC 是并发三色标记-清扫,搭配混合写屏障。
三色标记法
初始:所有对象都是白色
扫描根对象(全局变量、栈上的指针)→ 标记为灰色
遍历灰色对象,将其引用的白色对象标记为灰色
重复直到没有灰色对象
剩余的白色对象 → 垃圾,回收
| 颜色 | 含义 |
|---|---|
| ⚪ 白色 | 未扫描,可能是垃圾 |
| 🔘 灰色 | 已扫描自身,但引用的对象还没扫完 |
| ⚫ 黑色 | 已扫描自身及其引用,确定存活 |
步骤示意:
1. 初始
A(灰) B(白) C(白)
2. 扫描 A 的引用
A(黑) B(灰) C(白)
3. 扫描 B 的引用
A(黑) B(黑) C(灰)
4. 扫描 C 的引用(C 无引用其他对象)
A(黑) B(黑) C(黑)
5. 清扫白色对象 → 无,本轮无垃圾
写屏障
并发标记期间,mutator(用户代码)还在跑,可能产生新的引用。写屏障保证强/弱三色不变性:
插入屏障(1.5-1.7):
新创建的对象直接标为灰色。栈上对象无法被写屏障保护,需要 STW 重新扫描栈。
混合写屏障(1.8+):
结合插入屏障和删除屏障,栈不用 STW。满足「黑色对象不能引用白色对象」。
// Go 编译期自动插入写屏障指令
// 伪代码示意
func writePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
shade(ptr) // 混合屏障:新引用标记灰色
if currentStackIsGrey() {
shade(*slot) // 某些情况下旧引用也标记
}
*slot = ptr // 实际写入
}
GC 触发时机
- 内存分配达到阈值 — GOGC 默认 100(堆增长 100% 触发)— 主要触发方式
- 定时触发 — 上次 GC 后 2 分钟还没有 GC 则强制触发
- 手动触发 —
runtime.GC()
// 调试 GC
import "runtime"
import "runtime/debug"
debug.SetGCPercent(200) // 堆增长 200% 才触发,降低 GC 频率
runtime.GC() // 手动触发一次
debug.FreeOSMemory() // 归还内存给 OS
GC 四个阶段
1. 标记准备(STW 短暂) ← 启动写屏障
2. 并发标记(并发) ← 主要耗时
3. 标记终止(STW 短暂) ← 关闭写屏障,计算清扫工作
4. 并发清扫(并发) ← 回收白色对象
每个阶段的 STW 通常在微秒到亚毫秒级别。
GC 调优
// 查看 GC 情况
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("GC 次数: %d\n", m.NumGC)
fmt.Printf("累计 STW 暂停: %d ns\n", m.PauseTotalNs)
// 在测试中追踪 GC
GODEBUG=gctrace=1 go run main.go
// 输出示例:gc 1 @0.012s 2%: 0.012+0.68+0.012 ms
// gc 次数 时间 STW+并发标记+STW
调优方向:
- 减少堆分配 — 用
sync.Pool复用对象、避免不必要的[]byte转string - 调整 GOGC — 内存充足的场景调大,减少 GC 频率
- 预分配容量 —
make([]int, 0, 1000)避免扩容导致频繁分配 - 少用指针 — 指针多的结构体扫描成本高
总结
- Go GC 是并发三色标记 + 混合写屏障
- STW 只在标记准备和标记终止阶段,控制在微秒级
- 写屏障保证并发标记期间的正确性
- GOGC 控制 GC 触发频率
- 通过
sync.Pool、预分配等手段减少 GC 压力