Go 的垃圾回收器(GC)采用的是并发三色标记清除算法。为了理解它的运行过程,我们可以从宏观的“状态循环”到微观的“执行阶段”进行剖析。
宏观:三个状态循环
The GC continuously rotates through these three phases of sweeping, off, and marking in what’s known as the GC cycle. For the purposes of this document, consider the GC cycle starting with sweeping, turning off, then marking.
根据 Go 官方文档 (GC Guide),GC 的生命周期在一个持续旋转的状态机中运行:
- Sweeping (清理中):回收上一轮标记出的垃圾。
- Off (空闲):GC 未运行,等待触发条件(如堆内存增长、2 分钟定时)。
- Marking (标记中):扫描存活对象,这是 CPU 消耗最集中的阶段。
微观:四个执行阶段
虽然理论上是“标记-清除”,但为了支持并发运行,Go 在实现上将其细化为四个具体阶段。其中包含两次极短的 STW (Stop The World)。
第一阶段:Sweep Termination (清理终止)
- 状态:STW
- 目的:为标记阶段做准备,确保上一轮清理已彻底完成。
- 核心任务(有严格的先后顺序):
- 停止所有 Goroutine —— 为了看清楚现状。
- 清扫上一轮 GC 剩余的任务 —— 为了不被过去干扰。
- 开启写屏障(Write Barrier) —— 为了能一边干活一边不漏掉变化。
- 统计状态切换所需的元数据 —— 为了量化本轮目标。
为什么会有上一轮的遗留工作?为了减少停顿(STW),Go 不会一次性强迫所有内存都清扫干净才结束一轮清理。
第二阶段:Marking (并发标记)
- 状态:并发 (Concurrent)
- 目的:遍历对象图,标记所有存活对象。
- 核心任务:
- Root 扫描:扫描栈、全局变量和寄存器,将初始对象标记为灰色。
- 三色标记:并发地将灰色对象转为黑色,并将其引用的对象染灰。
- 协助标记(Mark Assist):如果用户协程分配内存过快,会被“强制拉壮丁”协助 GC 标记,防止堆失控。
- 写屏障维护:记录在标记期间发生的对象引用变化,确保并发安全性。
第三阶段:Mark Termination (标记终止)
- 状态:STW
- 目的:完成最后的标记收尾。
- 核心任务(有严格的先后顺序):
- 再次停止所有 Goroutine。
- 关闭写屏障。
- 处理写屏障缓冲区中剩余的对象。
- 计算下一轮 GC 触发的堆目标值(Pacer 算法)。
第四阶段:Sweeping (并发清理)
- 状态:并发 (Concurrent)
- 目的:回收白色对象的内存。
- 核心任务:
- 将标记为“死”的对象所属的内存块归还给
mspan的自由列表。 - 此阶段不影响用户协程运行,内存会在分配时被按需清理。
- 当所有垃圾清理完毕,GC 状态转回 Off,等待下一次触发。
- 将标记为“死”的对象所属的内存块归还给
核心机制小结
| 机制 | 作用 | 发生阶段 |
|---|---|---|
| STW | 确保数据一致性,切换 GC 状态 | 清理终止、标记终止 |
| 写屏障 | 防止并发标记时丢失存活对象 | 并发标记阶段全程 |
| 协助标记 | 负反馈调节,防止分配过快导致 OOM | 并发标记阶段 (按需) |
常见疑问解答
问:为什么 GC 周期的阶段有不同的答案
- 算法层 (2 阶段):Mark & Sweep 是核心算法逻辑。
- 状态层 (3 阶段):Sweeping → Off → Marking 是资源管理的宏观视角。
- 执行层 (4 阶段):是 Go 为了实现“低延迟并发”而在源码中定义的具体执行流,那两个额外的阶段本质上是 Marking 的“开始前准备”和“结束后收尾”。