Skip to main content

Documentation Index

Fetch the complete documentation index at: https://amigoer.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

理解 Go 的垃圾回收机制,掌握 GC 调优方法。

GC 算法演进

版本算法特点
Go 1.0STW全程暂停,延迟高
Go 1.3Mark STW标记阶段 STW
Go 1.5三色标记并发标记,STW 时间大幅降低
Go 1.8混合写屏障进一步降低 STW
Go 1.12优化内存分配减少内存碎片

三色标记法

三种颜色

┌─────────────────────────────────────────────────────────┐
│                    三色标记                              │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   白色 (White)                                          │
│   ├── 初始状态,所有对象都是白色                         │
│   └── GC 结束后,白色对象会被回收                        │
│                                                         │
│   灰色 (Gray)                                           │
│   ├── 已被扫描,但其引用的对象还未扫描                   │
│   └── 中间状态,待处理                                   │
│                                                         │
│   黑色 (Black)                                          │
│   ├── 已被扫描,其引用的对象也已扫描                     │
│   └── 存活对象,不会被回收                               │
│                                                         │
└─────────────────────────────────────────────────────────┘

标记流程

// 伪代码
func markPhase() {
    // 1. 初始化:所有对象标记为白色
    
    // 2. 根对象标记为灰色
    //    - 栈上变量
    //    - 全局变量
    //    - 寄存器
    
    // 3. 遍历灰色对象
    for len(grayObjects) > 0 {
        obj := grayObjects.pop()
        
        // 将引用的白色对象标记为灰色
        for _, ref := range obj.references() {
            if ref.color == white {
                ref.color = gray
                grayObjects.push(ref)
            }
        }
        
        // 将当前对象标记为黑色
        obj.color = black
    }
    
    // 4. 清除阶段:回收所有白色对象
}

并发标记问题

并发执行时可能出现对象丢失:
初始状态:
A(黑) ──→ B(灰) ──→ C(白)

并发修改后:
A(黑) ──→ C(白)  // A 直接引用 C
B(灰) ──✕ C      // B 不再引用 C

问题:C 永远不会被扫描到,被错误回收!

写屏障

插入写屏障(Dijkstra)

// 插入写屏障:当黑色对象引用白色对象时,将白色对象标记为灰色
func writePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
    shade(ptr)  // 将 ptr 标记为灰色
    *slot = ptr
}

// 问题:不适用于栈上对象
// 栈上对象不使用写屏障(性能考虑)
// 需要在标记结束时 STW 重新扫描栈

删除写屏障(Yuasa)

// 删除写屏障:当删除引用时,将被删除的对象标记为灰色
func writePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
    old := *slot
    shade(old)  // 将旧值标记为灰色
    *slot = ptr
}

// 问题:会导致一些垃圾延迟回收

混合写屏障(Go 1.8+)

// 混合写屏障:结合两种写屏障的优点
func writePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
    shade(*slot)  // 将旧值标记为灰色
    if current stack is gray {
        shade(ptr)  // 将新值标记为灰色
    }
    *slot = ptr
}

// 优点:
// 1. 不需要重新扫描栈
// 2. STW 时间极短

GC 流程

完整 GC 流程

┌─────────────────────────────────────────────────────────┐
│                      GC 流程                             │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  1. Mark Setup (STW)                                    │
│     ├── 开启写屏障                                       │
│     ├── 将根对象入队                                     │
│     └── 时间:约 10-30 微秒                              │
│                                                         │
│  2. Marking (并发)                                       │
│     ├── 后台 mark worker 扫描                            │
│     ├── 与用户程序并发执行                               │
│     └── 占用约 25% CPU                                   │
│                                                         │
│  3. Mark Termination (STW)                              │
│     ├── 关闭写屏障                                       │
│     ├── 清理处理器缓存                                   │
│     └── 时间:约 60-90 微秒                              │
│                                                         │
│  4. Sweeping (并发)                                      │
│     ├── 清理白色对象                                     │
│     └── 与用户程序并发执行                               │
│                                                         │
└─────────────────────────────────────────────────────────┘

GC 触发条件

// 1. 堆内存达到阈值
// 默认:堆大小达到上次 GC 后的 2 倍(GOGC=100)

// 2. 定时触发
// 2 分钟没有 GC,强制触发

// 3. 手动触发
runtime.GC()

// 4. 系统内存压力
// 当系统内存不足时

GC 调优

GOGC 参数

# GOGC 控制 GC 触发阈值
# 默认值 100,表示堆增长 100% 时触发 GC
GOGC=100 ./myapp

# GOGC=50:更频繁 GC,内存占用低,CPU 开销高
# GOGC=200:更少 GC,内存占用高,CPU 开销低
# GOGC=off:禁用 GC(危险!)

GOMEMLIMIT(Go 1.19+)

# 设置内存软限制
GOMEMLIMIT=1GiB ./myapp

# 当内存接近限制时,更积极地触发 GC
# 适合容器环境

代码层面优化

// 1. 复用对象
var bufferPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024)
    },
}

func process(data []byte) {
    buf := bufferPool.Get().([]byte)
    defer bufferPool.Put(buf)
    // 使用 buf...
}

// 2. 预分配
// Bad
var s []int
for i := 0; i < 10000; i++ {
    s = append(s, i)
}

// Good
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    s = append(s, i)
}

// 3. 避免逃逸
// Bad
func newBuffer() *bytes.Buffer {
    return &bytes.Buffer{}  // 逃逸到堆
}

// Good(如果可能)
func processWithBuffer(buf *bytes.Buffer) {
    // 由调用者管理 buffer
}

// 4. 使用值类型
// Bad
type Point struct{ X, Y float64 }
func (p *Point) Distance() float64 { ... }  // 可能导致逃逸

// Good
func (p Point) Distance() float64 { ... }  // 值拷贝,在栈上

监控 GC

// 1. 运行时监控
import "runtime/debug"

func monitorGC() {
    // 获取 GC 统计
    var stats debug.GCStats
    debug.ReadGCStats(&stats)
    
    fmt.Printf("GC 次数: %d\n", stats.NumGC)
    fmt.Printf("最近 GC 暂停: %v\n", stats.Pause[0])
    fmt.Printf("总暂停时间: %v\n", stats.PauseTotal)
}

// 2. 环境变量
// GODEBUG=gctrace=1 ./myapp
// 输出示例:
// gc 1 @0.012s 2%: 0.010+1.2+0.021 ms clock, 0.041+0.12/1.1/0+0.085 ms cpu, 4->4->0 MB, 5 MB goal, 4 P

// 解释:
// gc 1: 第 1 次 GC
// @0.012s: 程序启动后 0.012 秒
// 2%: GC 占用 CPU 时间百分比
// 0.010+1.2+0.021 ms: STW1 + 并发标记 + STW2 时间
// 4->4->0 MB: GC 前堆大小 -> GC 后堆大小 -> 存活对象大小
// 5 MB goal: 触发 GC 的堆大小目标

常见问题

GC 压力大

// 症状:CPU 使用率高,GC 频繁

// 排查:
// 1. 查看 GC 日志
// GODEBUG=gctrace=1 ./myapp

// 2. pprof 分析
go tool pprof http://localhost:6060/debug/pprof/heap

// 优化:
// 1. 增大 GOGC
// 2. 使用对象池
// 3. 减少小对象分配

内存泄漏

// 常见原因:
// 1. goroutine 泄漏
func leak() {
    ch := make(chan int)
    go func() {
        <-ch  // 永久阻塞
    }()
    // ch 永远不会发送数据
}

// 2. 全局变量持有引用
var cache = make(map[string]*Data)  // 只增不减

// 3. time.Ticker 未关闭
ticker := time.NewTicker(time.Second)
// 使用后要 ticker.Stop()

// 排查:
// 1. pprof heap 分析
// 2. goroutine 数量监控
// 3. top 命令查看内存增长