★ Go GC 的三色标记与写屏障是什么?

结论先行

Go 的 GC 是并发标记-清扫(mark & sweep)回收器,用三色抽象描述可达性标记:白色 = 尚未标记(垃圾候选)、灰色 = 已标记但引用还没扫完、黑色 = 已标记且引用扫完。标记从根对象出发把白色变灰、扫描灰对象引用再变黑,最终剩下的白色对象就是垃圾。并发标记期间为防“黑对象引用新白对象”导致误回收,需要写屏障维持不变式,Go 用的是混合写屏障。

要点

  • 三色过程:开始时所有对象白色;根(全局变量、goroutine 栈、寄存器)可达对象变灰入队;扫描灰对象的引用,把它们变灰、自己变黑;灰色清空后,白色即不可达垃圾。
  • 并发难点:标记与用户程序并行,用户可能让“已扫过的黑对象”重新指向“还没标记的白对象”,不保护就会误回收活对象。
  • 屏障思想:在指针写发生时插入检查代码,维护“黑不直接引用白”的强三色不变式(或其弱化版本),从机制上堵住漏标。
  • 混合写屏障:插入屏障(Dijkstra:黑引白时把白置灰)加删除屏障(Yuasa:删除对白的引用时把它置灰)组合,覆盖并发写场景。
  • 栈的处理:栈上的指针写不便频繁插屏障,Go 让各 goroutine 在安全点被扫描、并配合屏障,把全停顿压到极短。
  • 阶段节奏:标记准备(短暂 STW:开启写屏障、收集根)→ 并发标记(mark workers 与业务并行)→ 标记终止(短暂 STW:关屏障、收尾)→ 并发清扫(内存随分配惰性归还分配器)。

颜色速查

颜色含义后续处理
尚未标记,垃圾候选标记结束后回收
已标记,引用待扫描入队等待扫描
已标记,引用扫描完毕无需再处理

主动干预示例

正常线上不手动开关 GC,主要靠环境变量调节奏;下面代码仅供了解调试入口:

import "runtime/debug"

func main() {
    // 返回并修改 GC 目标百分比;-1 表示临时关闭自动触发(仅调试)
    prev := debug.SetGCPercent(-1)
    defer debug.SetGCPercent(prev)
}

实际调优优先使用 GOGCGOMEMLIMIT(Go 1.19+),先看监控里 GC 周期与 堆增长曲线,再决定是否干预,而不是在代码里到处 SetGCPercent。

和 Java G1 的粗略对照

维度Go GCJava G1
分代不分代,整堆标记分代 + Region 局部回收
停顿目标并发标记 + 并发清扫,毫秒级 STW可配置停顿时间目标
调参入口GOGC / GOMEMLIMIT-Xmx、-XX:MaxGCPauseMillis 等
适用服务端高并发程序大堆企业应用

对比的价值不在“谁更好”,而是说明:不同运行时的 GC 都在“标记-清扫”框架里 按自己的目标(延迟/吞吐)做取舍,原理相通。

常见追问/记忆点

  • 追问:为什么不全 STW 标记?答:STW 停顿与业务规模强相关,屏障让绝大多数标记与业务并行,停顿降到毫秒量级甚至更低。
  • 追问:插入屏障与删除屏障区别?答:插入管“新增引用”(黑→白置灰),删除管“被删引用”(被断开的白置灰);Go 混用两者覆盖堆上并发写。
  • 追问:三色标记会造成漏标还是多标?答:只会“多标”(浮动垃圾留到下一轮),配合屏障不会“漏标”活对象。
  • 记忆:白灰黑三步走;写屏障守“黑不指白”;混合屏障 = 插入 + 删除,换来近乎无 STW 的并发标记。
笔记加载中…