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)
}
}
解释
- 堆指针赋值 → 新对象标灰
黑色 → 白色 不可能出现
- 栈指针删除 → 老对象标黑
白色不会被孤立丢失
- 强三色不变式 100% 遵守
- 栈不需要 STW 扫描
七、强弱三色不变式
1. 强三色不变式
黑色对象绝不直接指向白色对象
黑 → 灰 → 白 ✅
黑 → 白 ❌(禁止)
2. 弱三色不变式
黑色可以指向白色,但白色必须被灰色引用
黑 → 白
灰 → 白 ✅
3. 混合写屏障效果
强制满足强三色不变式,最安全!
八、完整流程图
1. GC 全生命周期
2. 三色标记流程
3. 混合写屏障流程
4. 强弱三色不变式
九、概念
- 白色 = 未扫描、灰色 = 待扫、黑色 = 完成
- 强不变式:黑不能直接指向白
- 弱不变式:黑可以指向白,但必须有灰→白路径
- 混合写屏障 = 插入屏障 + 删除屏障
- 堆指针赋值 → 新对象标灰
- 栈指针删除 → 老对象标灰
- 遵守强三色不变式
- 彻底消除栈 STW
- GC 全程只有两次极短 STW(<1ms)
- Go 1.14+ 混合写屏障是业界最优 GC 方案
十、总结
- 三色标记:白死、灰扫、黑活
- 强不变式:黑不直说白
- 弱不变式:黑指白需灰保护
- 混合写屏障:堆插灰、栈删灰
- 强三色安全 + 无栈 STW + 低延迟