Go GC 概览
Go 的垃圾回收器是并发的、三色标记-清除收集器,设计目标是低延迟(减少 STW 停顿)而非极致吞吐。核心指标:STW 时间——Go 1.5 起已降到毫秒级,1.8 后通常 <1ms。
演进历程
- Go 1.0:简单标记-清除,STW 时间较长;
- Go 1.3:完全并发标记,显著减少停顿;
- Go 1.5:引入并发 GC + 三色标记,STW 降到毫秒级;
- Go 1.8:混合写屏障,STW 通常低于 1ms;
- Go 1.12+:持续优化大堆场景的延迟。
三色标记法
// 伪代码
func mark(start *Object) {
worklist := []*Object{start}
for len(worklist) > 0 {
obj := worklist.pop()
for _, ref := range obj.references() {
if ref.color == white {
ref.color = grey
worklist.push(ref)
}
}
obj.color = black
}
}
- 白色:未被访问(潜在垃圾);
- 灰色:已被访问,但其引用还没检查完;
- 黑色:自身和引用都已检查完。
标记结束后白色对象即垃圾。为保证并发标记期间对象图一致(不能让”黑对象引用白对象”导致活对象被误收),GC 引入写屏障:并发标记阶段发生指针写入时,把新引用的目标”染灰”。
GC 的四个阶段
GCoff
→ Mark Setup(STW:开启写屏障,准备标记)
→ Marking(并发:扫描并标记所有可达对象)
→ Mark Termination(STW:结束标记,关闭写屏障)
→ Sweeping(并发:回收未标记内存)
→ GCoff
注意:真正的 STW 只有 Mark Setup 和 Mark Termination 两小段,大头(Marking/Sweeping)都与程序并发执行。
触发时机与调优参数
GOGC
下次触发 GC 的堆大小 = 当前堆大小 × (1 + GOGC/100)
默认 GOGC=100:堆翻倍才触发。调小(如 50)→ 更频繁 GC、内存峰值低;调大(如 200)→ 更少 GC、内存占用高。大多数程序不需要调,只有内存敏感或延迟敏感场景才动。

观察 GC 日志
GODEBUG=gctrace=1 go run main.go
# 输出示例
gc 1 @0.012s 2%: 0.026+0.39+0.10 ms clock, ... 4->4->0 MB, 5 MB goal, 4 P
# | | | | |
# 序号 启动时间 GC占CPU% 各阶段耗时 堆大小变化 目标堆
字段含义:gc 1 序号、@0.012s 启动后时间、2% GC 占 CPU 比例、0.026+0.39+0.10 ms 依次是 STW/并发标记/其他耗时、4→4→0 MB GC 开始/结束/存活堆大小、5 MB goal 目标堆。
常见问题与排查
- 内存泄漏:GC 不回收”仍被引用”的对象。经典案例:全局 map 缓存无限增长——用容量上限、定期清理或过期策略;
- GC 频繁触发:大量短生命周期对象导致分配压力大,CPU 都花在 GC 上。对策:对象池(sync.Pool)、复用缓冲、减少逃逸到堆的分配;
- 长 STW:一般由超大堆(GB 级)或复杂对象图引起。对策:升级 Go 版本、拆分服务减小单进程堆、加 CPU(GC 并行)。
逃逸分析
对象分配在栈还是堆,由编译器的逃逸分析决定——栈上分配零 GC 成本:
// 小对象、不逃逸 → 栈
small := make([]int, 0, 10)
// 大对象或指针被返回/存全局 → 逃逸到堆
large := make([]int, 0, 10000)
// 查看逃逸结果
go build -gcflags="-m -m" your_file.go
日常写代码时:避免在热路径里把不该逃逸的对象搞逃逸(如意外返回局部变量指针、闭包捕获变量),能显著降低 GC 压力。
最佳实践
- 不要过早优化:大多数程序不需要 GC 调优,先用 pprof 确认瓶颈;
- 减少堆分配:对象复用(sync.Pool)、优先值类型、避免大对象;
- 监控:pprof heap + gctrace 日志观察 GC 频率与 STW;
- 保持 Go 版本更新:GC 在持续改进(如 Go 1.21/1.22 对内存和并发标记的优化)。



