Go GC 机制:从三色标记到混合写屏障
面试 Go 岗位必被问:Go 的 GC 原理是什么?为什么延迟低? 这篇文章从三色标记讲到混合写屏障,把 Go 内存回收讲透。
一、Go GC 的演进
| 版本 | 算法 | 特点 |
|---|---|---|
| Go 1.5 之前 | 标记-清除(Stop The World) | 延迟大,毫秒级停顿 |
| Go 1.5 | 三色标记 + 并发 | STW 降到微秒级 |
| Go 1.8+ | 三色标记 + 混合写屏障 | STW 极短,< 1ms 目标 |
| Go 1.19+ | 软内存限制(GOMEMLIMIT) | 内存调优不再只靠 GOGC |
二、三色标记:核心算法
把对象分成三种颜色:
流程:
三、并发标记的难题:为什么需要写屏障
GC 标记和业务 goroutine 是并发运行的。并发中最大的问题是"黑色对象引用白色对象"——扫描完了的黑色对象,被业务代码新加了一条指向白色对象的引用,而这个白色对象已经不会被扫描了,会被错误回收(对象丢失)。
解决:写屏障——业务代码每次写指针时,插入一段屏障逻辑。
插入写屏障(Go 1.5)
|
|
代价:每次指针写入都有额外开销;栈上的写屏障成本更高(栈读写频繁),所以 Go 1.5 时栈上不用写屏障,但需要重扫描栈(STW 的一部分)。
混合写屏障(Go 1.8+)
|
|
关键改进:
- 栈上不用写屏障,也不用重扫栈——栈对象全标黑(黑色安全假设),减少 STW
- 写屏障只在堆上做,栈写入零开销
- STW 只剩"标记终止"阶段,微秒级
这就是 Go 1.8 之后 GC 延迟大幅下降的核心。
四、触发时机与调优
触发条件
调优参数
|
|
GOMEMLIMIT 是最佳实践:限制 Go 进程最大内存,超了才加速 GC,配合 GOGC=off(纯靠内存限制触发)能避免在低内存场景下无限 GC。
pprof 看 GC 开销
|
|
判断垃圾对象:alloc_objects 高但 inuse_objects 低 = 高频临时对象,考虑对象池(sync.Pool)。
五、常见 GC 陷阱
陷阱 1:大量临时对象导致 GC 风暴
每秒创建百万个小对象 → GC 频繁触发 → CPU 全花在标记上。
解法:sync.Pool 复用 + 减少指针逃逸(-gcflags=-m 看逃逸分析)。
陷阱 2:指针逃逸到堆
|
|
解法:能返回值就返回值,别返回指针;sync.Pool 装热对象。
陷阱 3:大对象滞留
大 slice 只用了前 10 个元素,但底层数组还占着几 MB——append 后旧底层数组没被回收。
解法:copy 出小切片再赋值,释放大数组。
总结
Go GC 的核心设计思想是用写屏障换并发:
- 三色标记解决"怎么标",写屏障解决"并发下怎么保证不错标"
- 混合写屏障让栈免于扫描,STW 压到微秒级
- 调优靠
GOMEMLIMIT+sync.Pool+ 减少逃逸,而不是瞎调 GOGC
理解 GC,写 Go 时你会下意识地减少逃逸、控制对象大小——这不是优化,是基本素养。