Go 采用的是 三色并发标记清除算法。

传统的 GC 算法需要较长的 STW (Stop The World) 停顿时间(即暂停所有应用程序代码的运行来执行 GC)。

Go GC 的首要目标就是将 STW 停顿时间降到毫秒级甚至微秒级。

三色标记法

Go GC将堆上的所有对象分为三种颜色:

颜色 状态 描述
Black 已扫描存活 自身及其引用的所有对象都已被检查,确定为存活对象。黑色对象在本次 GC 周期内不会再被扫描。
White 未访问或待清除 尚未被 GC 访问,如果 GC 结束后仍是白色,则会被判定为垃圾并清除。
Gray 待扫描 自身已被标记为存活,但它引用的对象(子对象)尚未被扫描

工作流程:

  1. 初始状态: 所有的堆对象都是白色
  2. 根对象扫描: GC 从根对象(包括全局变量、活跃的 Goroutine 栈上的变量等)开始,将它们标记为灰色
  3. 遍历扩散: GC 不断从灰色集合中取出一个对象:
    • 将其引用的所有白色子对象标记为灰色。
    • 然后将该对象自身标记为黑色。
  4. 循环结束: 当灰色集合为空时,所有存活对象都已标记为黑色或灰色(如果不是根对象,灰色对象在下一轮会被标记为黑色),剩下的白色对象就是垃圾。

并发

Go GC 的大部分标记工作都是与用户程序并发进行的。这大大减少了 STW 停顿时间。

  • 实现: GC 作为一个或多个特殊的 Goroutine 运行,与用户程序共享 CPU 资源。

写屏障

并发带来的最大挑战是:当 GC 在标记对象时,用户程序可能会更改指针引用,导致 GC 出现错误标记。

错误标记的两种可能:

错误类型 发生场景 结果
丢失标记 应用程序在标记过程中将白色对象连接到了黑色对象,同时切断了该白色对象与灰色对象的唯一联系。 该存活对象被误判为垃圾,最终被清除(程序崩溃!)。
**重复标记 ** 应用程序将一个 白色对象 连接到了 灰色对象 影响效率,但不会导致错误。

为了解决丢失标记这一致命问题,Go 在 1.8 版本后采用了 混合写屏障。其核心思想是:

  1. 在指针写入前(Pre-Write):如果当前要被覆盖的指针指向一个对象 A,则将对象 A 标记为灰色(即保证其被扫描)。
  2. 栈扫描时 STW: 在 GC 过程中,栈上的对象处理(扫描)必须在 STW 期间进行(非常短),以避免写屏障对栈操作的影响。

** 写屏障保证了在并发标记期间,所有存活的对象**都不会被误标为白色并清除。

GC Pacing

Go GC 并非盲目地等待内存不足才运行,而是通过 Pacer 机制来规划运行时间,以平衡性能和内存消耗。

  • GOGC 变量: 环境变量 GOGC 控制了目标堆大小。默认值是 100
  • 触发目标: 当当前堆内存使用量达到上次 GC 完成后存活对象大小的 (1 + GOGC/100) 倍时,GC 就会开始。
    • 例如,如果 GOGC=100,上次活对象是 10MB,那么当堆内存增长到 20MB 时,GC 就会启动。

GC Cycle

一个完整的 GC 循环

  1. 标记启动:暂停所有 Goroutine;开启写屏障,设置 GC 状态。

  2. 并发标记

    • 并发执行: GC Goroutine 开始扫描堆,执行三色标记,同时用户程序也在运行。

    • 写屏障介入: 应用程序的所有指针操作都受到写屏障的监控。

  3. 标记终止

    • STW 暂停所有 Goroutine,进行最后的清理和同步,包括扫描栈上的对象以确保万无一失。

    • 关闭: 关闭写屏障,确认所有存活对象已标记。

  4. 并发清除:GC 遍历整个堆内存,回收所有未被标记(白色)的对象,并将空闲的内存块合并到空闲链表中,供下次分配使用。