HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • Go 架构进阶

    • Go 架构进阶学习手册 - 总目录
    • 01-GMP调度模型深度解析
    • 02-Channel源码剖析
    • 03-内存模型与GC机制
    • 04-垃圾回收器全链路
    • 05-并发模型与锁机制
    • 06-网络模型与Netpoll
    • 07-Runtime全景融合
    • 08-性能优化实战
    • 09-微服务架构实践

04-垃圾回收器全链路

章节概述

Go 语言的垃圾回收器采用三色标记算法,结合写屏障机制实现并发垃圾回收。本章将深入解析 GC 的设计原理、源码实现和优化策略,帮助读者理解 Go 如何实现低延迟的垃圾回收。

学习目标

  • 理解三色标记算法的原理
  • 掌握写屏障机制的作用
  • 了解并发标记和清扫的实现
  • 学会 GC 调优和性能分析
  • 能够识别和解决 GC 相关问题

️ GC 架构概览

三色标记模型

┌─────────────────────────────────────────────────────────┐
│                   三色标记模型                          │
├─────────────────────────────────────────────────────────┤
│  白色 (White)    灰色 (Gray)     黑色 (Black)          │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐    │
│  │ 未访问对象   │  │ 已发现未扫描│  │ 已扫描完毕   │    │
│  │ 候选垃圾     │  │ 待处理队列  │  │ 存活对象     │    │
│  └─────────────┘  └─────────────┘  └─────────────┘    │
└─────────────────────────────────────────────────────────┘

GC 阶段划分

┌─────────────────────────────────────────────────────────┐
│                    GC 执行阶段                          │
├─────────────────────────────────────────────────────────┤
│  1. 标记开始 (STW)    2. 并发标记     3. 标记结束 (STW)  │
│  ┌─────────────┐     ┌─────────────┐  ┌─────────────┐  │
│  │ 暂停所有 G   │     │ 与用户并发   │  │ 重新扫描     │  │
│  │ 扫描根对象   │     │ 标记对象     │  │ 完成标记     │  │
│  └─────────────┘     └─────────────┘  └─────────────┘  │
│                                                         │
│  4. 并发清扫                                            │
│  ┌─────────────┐                                        │
│  │ 清理垃圾对象│                                        │
│  │ 回收内存    │                                        │
│  └─────────────┘                                        │
└─────────────────────────────────────────────────────────┘

核心数据结构

GC 状态定义

文件位置:src/runtime/mgc.go

const (
    _GCoff             = iota // GC 关闭
    _GCmark                   // 标记阶段
    _GCmarktermination        // 标记结束阶段
    _GCsweep                  // 清扫阶段
)

type gcController struct {
    // GC 触发条件
    triggerRatio float64
    heapGoal     uint64
    heapLive     uint64
    heapMarked   uint64
    
    // 工作统计
    scanWork int64
    bgScanCredit int64
    
    // 辅助标记
    assistTime int64
    dedicatedMarkWorkersNeeded int64
}

标记工作结构

type gcWork struct {
    wbuf1, wbuf2 *workbuf
    bytesMarked  uint64
    scanWork     int64
    flushedWork  bool
}

type workbuf struct {
    workbufhdr
    obj [(_WorkbufSize - unsafe.Sizeof(workbufhdr{})) / sys.PtrSize]uintptr
}

type workbufhdr struct {
    node lfnode // 必须是第一个字段
    nobj int
}

GC 触发机制

触发条件

func (c *gcController) trigger() gcTrigger {
    // 1. 堆大小触发
    if c.heapLive >= c.heapGoal {
        return gcTriggerHeap
    }
    
    // 2. 时间触发
    if c.heapLive >= c.heapMarked+c.heapMarked/4 {
        return gcTriggerTime
    }
    
    // 3. 手动触发
    if c.heapLive >= c.heapMarked+c.heapMarked/2 {
        return gcTriggerManual
    }
    
    return gcTriggerNone
}

触发参数

// GOGC 参数控制
func (c *gcController) update() {
    // 计算触发比例
    c.triggerRatio = float64(gcpercent) / 100.0
    
    // 计算目标堆大小
    c.heapGoal = c.heapMarked + uint64(float64(c.heapMarked)*c.triggerRatio)
    
    // 更新统计信息
    c.heapLive = atomic.Load64(&c.heapLive)
    c.heapMarked = atomic.Load64(&c.heapMarked)
}

三色标记算法

标记过程

func gcMark() {
    // 1. 初始化标记
    gcMarkRoots()
    
    // 2. 并发标记
    for {
        // 从灰色队列获取对象
        obj := gcWork.tryGet()
        if obj == nil {
            break
        }
        
        // 扫描对象
        scanObject(obj)
    }
    
    // 3. 完成标记
    gcMarkDone()
}

func scanObject(obj uintptr) {
    // 获取对象类型
    typ := getType(obj)
    
    // 扫描对象字段
    for i := 0; i < typ.numField; i++ {
        field := obj + typ.fieldOffset[i]
        if isPointer(field) {
            // 标记引用的对象
            markObject(*(*uintptr)(unsafe.Pointer(field)))
        }
    }
}

根对象标记

func gcMarkRoots() {
    // 1. 标记全局变量
    for _, v := range globalVars {
        markObject(v)
    }
    
    // 2. 标记栈变量
    for _, g := range allgs {
        markStack(g)
    }
    
    // 3. 标记寄存器
    markRegisters()
}

func markStack(g *g) {
    // 扫描 goroutine 栈
    for sp := g.stack.lo; sp < g.stack.hi; sp += sys.PtrSize {
        if isPointer(sp) {
            markObject(*(*uintptr)(unsafe.Pointer(sp)))
        }
    }
}

️ 写屏障机制

写屏障原理

写屏障是保证并发 GC 正确性的关键机制,当程序修改指针时,写屏障会检查并标记新引用的对象。

// 写屏障函数
func gcWriteBarrier(dst *uintptr, src uintptr) {
    if writeBarrierEnabled {
        if src != 0 {
            // 标记源对象
            markObject(src)
        }
    }
    *dst = src
}

写屏障类型

1. 插入写屏障 (Insert Barrier)

func insertBarrier(dst *uintptr, src uintptr) {
    if src != 0 {
        // 标记新引用的对象
        markObject(src)
    }
    *dst = src
}

2. 删除写屏障 (Delete Barrier)

func deleteBarrier(dst *uintptr, old uintptr) {
    if old != 0 {
        // 标记被删除引用的对象
        markObject(old)
    }
    *dst = 0
}

写屏障实现

文件位置:src/runtime/mbarrier.go

func gcWriteBarrier(dst *uintptr, src uintptr) {
    if writeBarrierEnabled {
        if src != 0 {
            // 检查是否需要标记
            if src < gcController.heapMarked {
                // 对象在标记开始前分配,需要标记
                markObject(src)
            }
        }
    }
    *dst = src
}

并发标记实现

标记工作器

func gcMarkWorker() {
    for {
        // 获取标记工作
        work := gcWork.tryGet()
        if work == nil {
            // 没有工作,休眠
            gopark(nil, nil, waitReasonGCMarkWorker, traceEvGoBlock, 0)
            continue
        }
        
        // 执行标记工作
        scanObject(work)
        
        // 更新统计
        atomic.AddInt64(&gcController.scanWork, 1)
    }
}

辅助标记

func gcAssistAlloc(gp *g) {
    // 计算需要辅助的工作量
    assistWork := gp.gcAssistBytes
    
    if assistWork > 0 {
        // 执行辅助标记
        for assistWork > 0 {
            obj := gcWork.tryGet()
            if obj == nil {
                break
            }
            
            scanObject(obj)
            assistWork--
        }
    }
}

并发清扫

清扫过程

func gcSweep() {
    // 1. 清扫 span
    for {
        s := mheap_.sweepSpans[0].pop()
        if s == nil {
            break
        }
        
        // 清扫 span
        if s.sweep() {
            // span 已清空,放回 mcentral
            mheap_.central[s.spanclass].mcentral.cacheSpan()
        }
    }
    
    // 2. 清扫大对象
    for _, obj := range mheap_.largeObjects {
        if !obj.marked {
            // 释放大对象
            mheap_.freeLarge(obj)
        }
    }
}

增量清扫

func (s *mspan) sweep() bool {
    // 清扫对象
    for i := s.freeindex; i < s.nelems; i++ {
        if !s.allocBits.isMarked(i) {
            // 对象未标记,加入空闲列表
            s.freeList.push(s.base() + i*s.elemsize)
        }
    }
    
    // 更新 span 状态
    s.allocCount = s.freeList.len()
    s.freeindex = s.nelems
    
    return s.allocCount == 0
}

️ 实战代码

1. GC 状态监控

package main

import (
    "fmt"
    "runtime"
    "time"
)

func monitorGC() {
    var m runtime.MemStats
    
    for {
        runtime.ReadMemStats(&m)
        
        fmt.Printf("=== GC 状态监控 ===\n")
        fmt.Printf("堆大小: %d KB\n", m.HeapAlloc/1024)
        fmt.Printf("堆目标: %d KB\n", m.NextGC/1024)
        fmt.Printf("GC 次数: %d\n", m.NumGC)
        fmt.Printf("GC 暂停: %v\n", time.Duration(m.PauseTotalNs))
        fmt.Printf("上次 GC: %v\n", time.Unix(0, int64(m.LastGC)))
        fmt.Printf("GC 频率: %.2f/s\n", float64(m.NumGC)/time.Since(time.Unix(0, int64(m.LastGC))).Seconds())
        
        time.Sleep(5 * time.Second)
    }
}

func main() {
    go monitorGC()
    
    // 模拟内存分配
    for i := 0; i < 1000; i++ {
        data := make([]byte, 1024*1024) // 1MB
        _ = data
        
        if i%100 == 0 {
            runtime.GC() // 手动触发 GC
        }
        
        time.Sleep(100 * time.Millisecond)
    }
}

2. GC 压力测试

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func gcPressureTest() {
    var wg sync.WaitGroup
    
    // 创建大量 goroutine
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            // 频繁分配内存
            for j := 0; j < 1000; j++ {
                data := make([]byte, 1024) // 1KB
                _ = data
                
                if j%100 == 0 {
                    runtime.Gosched() // 让出 CPU
                }
            }
        }(i)
    }
    
    wg.Wait()
}

func main() {
    // 设置 GC 参数
    runtime.GOMAXPROCS(4)
    
    // 记录开始时间
    start := time.Now()
    
    // 运行压力测试
    gcPressureTest()
    
    // 记录结束时间
    duration := time.Since(start)
    
    // 输出统计信息
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    fmt.Printf("测试完成,耗时: %v\n", duration)
    fmt.Printf("GC 次数: %d\n", m.NumGC)
    fmt.Printf("GC 暂停总时间: %v\n", time.Duration(m.PauseTotalNs))
    fmt.Printf("平均 GC 暂停: %v\n", time.Duration(m.PauseTotalNs)/time.Duration(m.NumGC))
}

3. GC 调优工具

package main

import (
    "fmt"
    "runtime"
    "time"
)

type GCTuner struct {
    targetPause    time.Duration
    currentPause   time.Duration
    gcPercent      int
    heapSize       uint64
    lastGC         time.Time
}

func NewGCTuner() *GCTuner {
    return &GCTuner{
        targetPause: 10 * time.Millisecond,
        gcPercent:   100,
    }
}

func (t *GCTuner) Tune() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    // 计算当前 GC 暂停时间
    if m.NumGC > 0 {
        t.currentPause = time.Duration(m.PauseTotalNs) / time.Duration(m.NumGC)
    }
    
    // 调整 GC 参数
    if t.currentPause > t.targetPause {
        // GC 暂停时间过长,增加 GOGC
        t.gcPercent += 10
        if t.gcPercent > 200 {
            t.gcPercent = 200
        }
    } else if t.currentPause < t.targetPause/2 {
        // GC 暂停时间过短,减少 GOGC
        t.gcPercent -= 10
        if t.gcPercent < 50 {
            t.gcPercent = 50
        }
    }
    
    // 应用调整
    runtime.GC()
    
    fmt.Printf("GC 调优: 暂停时间=%v, 目标=%v, GOGC=%d\n", 
        t.currentPause, t.targetPause, t.gcPercent)
}

func main() {
    tuner := NewGCTuner()
    
    // 定期调优
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            tuner.Tune()
        }
    }
}

GC 性能分析

性能指标

  1. GC 频率:单位时间内的 GC 次数
  2. GC 暂停时间:单次 GC 的暂停时间
  3. GC 吞吐量:GC 处理的内存量
  4. 内存利用率:堆内存的使用效率

性能测试

package main

import (
    "fmt"
    "runtime"
    "time"
)

func benchmarkGC() {
    var m runtime.MemStats
    
    // 记录开始状态
    runtime.ReadMemStats(&m)
    startGC := m.NumGC
    startPause := m.PauseTotalNs
    
    // 运行测试
    start := time.Now()
    for i := 0; i < 1000000; i++ {
        data := make([]byte, 1024)
        _ = data
    }
    duration := time.Since(start)
    
    // 记录结束状态
    runtime.ReadMemStats(&m)
    endGC := m.NumGC
    endPause := m.PauseTotalNs
    
    // 计算性能指标
    gcCount := endGC - startGC
    totalPause := time.Duration(endPause - startPause)
    avgPause := totalPause / time.Duration(gcCount)
    
    fmt.Printf("测试结果:\n")
    fmt.Printf("  运行时间: %v\n", duration)
    fmt.Printf("  GC 次数: %d\n", gcCount)
    fmt.Printf("  GC 总暂停: %v\n", totalPause)
    fmt.Printf("  平均暂停: %v\n", avgPause)
    fmt.Printf("  GC 频率: %.2f/s\n", float64(gcCount)/duration.Seconds())
}

func main() {
    benchmarkGC()
}

面试题库

基础问题

  1. 什么是三色标记算法?

    • 白色:未访问对象,候选垃圾
    • 灰色:已发现但未扫描
    • 黑色:已扫描完毕,存活对象
  2. 写屏障的作用是什么?

    • 保证并发 GC 的正确性
    • 标记新引用的对象
    • 维护"黑不指白"不变式
  3. Go GC 的触发条件?

    • 堆大小达到目标
    • 时间触发
    • 手动触发

进阶问题

  1. 为什么 Go 的 GC 延迟低?

    • 并发标记和清扫
    • 增量清扫
    • 写屏障优化
    • 辅助标记机制
  2. 如何优化 GC 性能?

    • 调整 GOGC 参数
    • 减少内存分配
    • 使用对象池
    • 避免内存泄漏
  3. GC 的 STW 时间如何优化?

    • 减少根对象扫描时间
    • 优化写屏障
    • 使用并发标记
    • 增量清扫

源码问题

  1. gcMark 函数的主要流程?

    • 标记根对象
    • 并发标记
    • 完成标记
  2. 写屏障的实现原理?

    • 检查指针修改
    • 标记新引用对象
    • 维护标记状态

扩展阅读

  • Go GC 源码分析
  • Go 内存管理
  • Go 性能优化
  • Go 内存模型

相关章节

  • 01-GMP调度模型深度解析 - GC 与调度的协作
  • 03-内存模型与GC机制 - 内存分配与 GC 的关系
  • 08-性能优化实战 - GC 性能调优

下一章预告:我们将深入 Go 的并发模型和锁机制,了解 Mutex、RWMutex、Atomic 等同步原语的实现原理。

Prev
03-内存模型与GC机制
Next
05-并发模型与锁机制