Go 垃圾回收

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 对内存和并发标记的优化)。
滚动至顶部