go从零单排之GC

0 阅读4分钟

Go GC 三色标记清扫 + 强弱三色不变式 + 混合写屏障


一、3 大核心概念

1. 三色标记(基础)

  • 白色:未扫描 → 可回收
  • 灰色:自身活、子对象未扫描 → 待处理
  • 黑色:自身 + 子对象都扫描 → 存活

2. 强弱三色不变式(GC 安全底线)

这是 并发 GC 不丢对象 的核心规则!

✔ 强三色不变式(严格)

黑色对象 绝对不能 指向 白色对象

(一旦指向,白色对象会被误删)

✔ 弱三色不变式(宽松)

黑色对象指向白色对象时,白色对象必须被灰色对象间接引用

(只要有一条灰色→白色路径,就不会丢)

结论

只要遵守任意一种,GC 就安全不丢对象!

3. 混合写屏障(Go 1.14+)

同时满足:堆插入屏障 + 栈删除屏障

= 彻底遵守强三色不变式

= 完全消除栈 STW

= GC 最安全、最低延迟


二、为什么会丢对象?

1. 黑色指向白色(强不变式破坏)

黑色 A → 白色 B

B 没有灰色引用

GC 会把 B 删掉!

2. 白色被剥离(弱不变式破坏)

灰色 C → 白色 B

指针被改成 A→B,C 不再指向 B

B 变成孤立白色,被误删!


三、混合写屏障如何同时解决?

混合写屏障 = 插入写屏障 + 删除写屏障

1. 插入写屏障(堆对象)

只要新指针指向一个对象,就把它标灰

→ 杜绝黑色直接指向白色

2. 删除写屏障(栈对象)

删除指针时,老对象直接变黑

→ 杜绝白色被剥离

最终效果

全程遵守强三色不变式,绝对安全!


四、GC 核心结构体

文件:runtime/mgc.go runtime/bitmap.go

// ========== GC 阶段 ==========
const (
        _GCoff             = iota // 关闭,清扫中
        _GCmark                  // 并发标记
        _GCmarktermination       // 标记结束(STW)
)
// ========== 全局GC控制器 ==========
var gcController struct {
        phase       uint32     // 当前阶段
        markWork    uint64     // 标记任务量
        heapGoal    uint64     // GC触发阈值
}
// ========== 写屏障全局开关 ==========
var writeBarrier struct {
        enabled bool    // 标记阶段=true
        _       [7]byte // 对齐
}
// ========== GC 标记位图 ==========
type gcBits struct {
        bitp *uint8  // 标记位指针
        bit  uint8   // 当前位
}

五、GC 全生命周期源码

1. GC 入口 gcStart()

// GC 启动主流程
func gcStart(trigger gcTrigger) {
        // 1. 同步完成上一轮清扫
        sweepWait()
        // 2. 标记准备阶段(STW)
        gcMarkSetup()
        // 3. 并发标记(与业务并行)
        gcConcurrentMark()
        // 4. 标记结束(STW)
        gcMarkTermination()
        // 5. 并发清扫
        sweepStart()
}

2. 标记准备(STW + 开启写屏障)

func gcMarkSetup() {
        // ---------- STW 极短 ----------
        stopTheWorld("GC mark setup")
        // 设置为标记阶段
        gcController.phase = _GCmark
        // 重置标记位图
        gcResetMarkBits()
        // ========== 开启混合写屏障 ==========
        writeBarrier.enabled = true
        // 扫描根对象(栈、全局变量、寄存器)
        gcMarkRoots()
        startTheWorld()
}

3. 并发标记

func gcConcurrentMark() {
        // 与用户 goroutine 并行执行
        for {
                // 从灰色队列取对象
                gp := getGrayObject()
                if gp == nil {
                        break // 标记完成
                }
                // 扫描子对象(子对象标灰)
                scanObject(gp)
                // 当前对象标黑
                markBlack(gp)
        }
}

4. 对象扫描

// scan 扫描灰色对象的所有指针字段
func scanObject(obj unsafe.Pointer) {
        // 遍历对象所有指针
        for ptr := range obj.pointers() {
                target := *ptr
                // 子对象标灰
                if !isMarked(target) {
                        markGray(target)  // 放入灰色队列
                        addWork(target)
                }
        }
}

5. 标记结束(STW + 关闭写屏障)

func gcMarkTermination() {
        stopTheWorld("GC mark termination")
        // 关闭混合写屏障
        writeBarrier.enabled = false
        // 切换到清扫阶段
        gcController.phase = _GCoff
        startTheWorld()
}

6. 并发清扫

func sweepWork() {
        // 遍历所有 span
        for span := range allSpans {
                if span.state == mSpanInUse {
                        // 清扫白色对象 → 释放内存
                        sweepSpan(span)
                }
        }
}

六、混合写屏障

文件:runtime/asm_amd64.go + runtime/mgc_wb.go

写屏障总入口(指针赋值自动触发)

// 写屏障:每次执行 指针A = 指针B 都会调用
func writeBarrierMix(slot *unsafe.Pointer, newValue unsafe.Pointer) {
        // ========== 规则1:插入写屏障(堆指针) ==========
        // 如果 slot 在堆上,新指向的对象标灰
        if inHeap(uintptr(unsafe.Pointer(slot))) {
                shade(newValue) // 标灰
        }
        // ========== 规则2:删除写屏障(栈指针) ==========
        // 如果 slot 在栈上,老对象直接标黑
        if inStack(uintptr(unsafe.Pointer(slot))) {
                oldValue := *slot
                shade(oldValue)
        }
}
// shade = 标记灰色(保证存活)
func shade(ptr unsafe.Pointer) {
        if !isMarked(ptr) {
                markGray(ptr)
        }
}

解释

  1. 堆指针赋值 → 新对象标灰

黑色 → 白色 不可能出现

  1. 栈指针删除 → 老对象标黑

白色不会被孤立丢失

  1. 强三色不变式 100% 遵守
  1. 栈不需要 STW 扫描

七、强弱三色不变式

1. 强三色不变式

黑色对象绝不直接指向白色对象

黑 → 灰 → 白 ✅
黑 → 白 ❌(禁止)

2. 弱三色不变式

黑色可以指向白色,但白色必须被灰色引用

黑 → 白
灰 → 白  ✅

3. 混合写屏障效果

强制满足强三色不变式,最安全!


八、完整流程图

1. GC 全生命周期

image.png

2. 三色标记流程

image.png

3. 混合写屏障流程

image.png

4. 强弱三色不变式

image.png


九、概念

  1. 白色 = 未扫描、灰色 = 待扫、黑色 = 完成
  1. 强不变式:黑不能直接指向白
  1. 弱不变式:黑可以指向白,但必须有灰→白路径
  1. 混合写屏障 = 插入屏障 + 删除屏障
  1. 堆指针赋值 → 新对象标灰
  1. 栈指针删除 → 老对象标灰
  1. 遵守强三色不变式
  1. 彻底消除栈 STW
  1. GC 全程只有两次极短 STW(<1ms)
  1. Go 1.14+ 混合写屏障是业界最优 GC 方案

十、总结

  • 三色标记:白死、灰扫、黑活
  • 强不变式:黑不直说白
  • 弱不变式:黑指白需灰保护
  • 混合写屏障:堆插灰、栈删灰
  • 强三色安全 + 无栈 STW + 低延迟