摘要:从三色标记法、并发回收与写屏障机制,到 Go 1.26 默认启用的 Green Tea GC——以页为单位的批量扫描如何取代传统对象图遍历,提升缓存局部性与标记效率。
Go GC 的基本目的
垃圾回收器负责识别堆中已经无法被程序访问的对象,并将其占用的内存重新交给内存分配器使用。栈上对象通常随函数栈帧自动释放,GC 主要管理逃逸到堆上的对象。
Go GC 起点
- Go 1.5:引入精确、并行、并发的标记—清扫垃圾回收器,大部分标记工作可以和用户程序并发执行。
- Go 1.8:引入混合写屏障,将 Yuasa 删除写屏障和 Dijkstra 插入写屏障结合起来,主要解决并发标记时的栈重扫描问题,进一步缩短 Stop-The-World 时间。
- Go 1.26:默认启用新的 Green Tea GC, 思想核心:“Work with pages, not objects”。
三色标记法
Golang GC 用到的三色标记法属于标记清扫-算法的一种实现,核心点有:
- 将对象分为白色、灰色和黑色三种状态
- 白色表示对象尚未被扫描,可能是垃圾对象
- 灰色表示对象已经确认存活,但其引用的对象尚未扫描完成
- 黑色表示对象已经确认存活,并且其引用的对象已经扫描完成
- 初始时所有对象为白色,将根集合直接引用的对象标记为灰色
- 不断扫描灰色对象,将其引用的白色对象标记为灰色,扫描完成后将当前对象标记为黑色
- 当不存在灰色对象时,剩余的白色对象就是不可达的垃圾对象
可以将标记工作拆分执行,适合实现增量和并发垃圾回收,能够减少一次性标记造成的长时间停顿。
并发垃圾回收
Go 1.5 版本之前,GC 时需要停止全局的用户协程,专注完成 GC 工作后回恢复用户协程。之后引入并发垃圾回收机制,允许用户协程和 GC 协程并发运行。
并发标记期间应用程序可能修改对象引用,导致存活对象被错误回收,需要通过写屏障等机制维护“黑色对象不能直接指向白色对象”的不变量。
屏障机制
3.1 强弱三色不变式
漏标问题的本质就是,一个已经扫描完成的黑对象指向了一个被灰 / 白对象删除引用的白色对象. 构成这一场景的要素拆分如下:
(1)黑色对象指向了白色对象
(2)灰、白对象删除了白色对象
(3)(1)、(2)步中谈及的白色对象是同一个对象
(4)(1)发生在(2)之前
一套用于解决漏标问题的方法论称之为强弱三色不变式:
- 强三色不变式:白色对象不能被黑色对象直接引用(直接破坏(1))
- 弱三色不变式:白色对象可以被黑色对象引用,但要从某个灰对象出发仍然可达该白对象(间接破坏了(1)、(2)的联动)
插入写屏障
插入写屏障(Dijkstra)的目标是实现强三色不变式,保证当一个黑色对象指向一个白色对象前,会先触发屏障将白色对象置为灰色,再建立引用.
删除写屏障
删除写屏障(Yuasa barrier)的目标是实现弱三色不变式,保证当一个白色对象即将被上游删除引用前,会触发屏障将其置灰,之后再删除上游指向其的引用.
混合写屏障
- GC 开始前,以栈为单位分批扫描,将栈中所有对象置黑
- GC 期间,栈上新创建对象直接置黑
- 堆对象正常启用插入写屏障
- 堆对象正常启用删除写屏障
Green Tea
Go 1.25 首次引入 Green Tea 垃圾回收器。该版本中,Green Tea 仍属于实验性功能,默认不会启用,需要在编译时显式设置:
GOEXPERIMENT=greenteagc go build才能使用新的垃圾回收实现。
从 Go 1.26 开始,Green Tea 被正式设置为 Go 的默认垃圾回收器。使用 Go 1.26 编译程序时,无需设置任何实验选项,即可自动采用 Green Tea GC。
核心思想:Work with pages, not objects
传统 GC:以对象为工作单位

传统 Go 垃圾回收器采用对象图遍历方式进行标记。GC 从根集合出发,将发现的对象加入对象工作列表,随后逐个取出对象,扫描其中的指针字段,并继续标记其引用的其他对象。
由于存在引用关系的对象不一定连续分布,它们可能位于不同的内存页,甚至位于同一内存页的不同区域。因此,GC 在沿指针遍历对象图时,往往需要频繁地在不同内存位置之间跳转,导致内存访问较为分散,CPU 缓存局部性较差。每个内存页都维护相应的元数据,其中的标记位用于记录页内各对象是否已经被发现。借助标记位,同一个对象在一次标记阶段中最多只会被加入对象工作列表一次,不会被重复标记。传统对象工作列表采用后进先出方式,整体表现为类似栈的深度优先遍历。
可以将传统过程概括为:
发现对象 ↓设置对象标记位 ↓将对象加入工作栈 ↓取出并扫描该对象 ↓沿对象中的指针访问其他对象这种方式的问题不在于对象会被重复标记,而在于扫描过程中需要沿着对象引用关系频繁跳转。引用关系越复杂、对象分布越分散,GC 越难形成连续的内存访问。
Green Tea:以页为工作单位
Green Tea 并没有放弃对象级别的精确标记,它改变的是标记工作的组织和调度方式:工作列表中不再主要保存单个对象,而是保存对象所在的内存页。
严格来说,Go 官方博客为了便于解释使用了“页(page)”这一概念,而运行时实现中主要以包含一组同尺寸对象的 span 作为批量扫描和排队单位。因此,可以将其理解为:
对象仍然是存活性判断的基本单位,但页或 span 成为了标记扫描的批处理单位。
Green Tea 为页内每个对象维护两组状态位:
- seen 位:表示 GC 已经发现了指向该对象的指针,即该对象已经确认可达。
- scanned 位:表示该对象本身已经扫描完成,其内部包含的指针也已经被处理。
当 GC 首次发现某个对象时,会设置该对象的 seen 位,并将它所在的页或 span 加入工作队列,而不是直接将对象本身加入工作列表。

Green Tea 的扫描过程
当 GC 从工作队列中取出一个页时,会比较该页的 seen 位图和 scanned 位图:
待扫描对象 = seen - scanned也就是查找满足以下条件的对象:
seen = 1scanned = 0这些对象已经被发现,但其内部指针尚未扫描。GC 会按照内存中的排列顺序,连续扫描该页中的多个待处理对象,并将相应的 scanned 位设置为 1。

在扫描这些对象的过程中:
- 如果发现它们指向另一个对象,则设置目标对象的
seen位。 - 如果目标对象所在的页尚未进入工作队列,则将该页加入队列。
- 如果目标页已经位于工作队列中,则不必重复入队,只需要更新目标对象的
seen位。 - 如果某个页此前已经处理完毕,但之后又发现了该页中的其他对象,该页可以再次加入工作队列。
- 即使一个页能够多次入队,已经设置
scanned位的对象也不会被重复扫描。
整个过程可以概括为:
发现对象 ↓设置对象的 seen 位 ↓将对象所在页加入工作队列 ↓从队列中取出一页 ↓计算 seen - scanned ↓连续扫描该页中的所有待扫描对象 ↓更新 scanned 位 ↓将新发现对象所在的页加入工作队列当工作队列为空,并且所有可达对象都满足:
seen = scanned = 1说明所有可达对象都已完成扫描,标记阶段结束。没有设置 seen 位的对象属于不可达对象,可以在后续清扫阶段被回收。
为什么采用先进先出的页队列
传统对象图遍历使用的是类似栈的后进先出策略,而 Green Tea 的页或 span 工作列表采用先进先出的队列策略。这样可以让某个页在队列中等待一段时间,使更多待扫描对象逐渐积累到该页中。当该页最终出队时,GC 就能一次连续处理多个对象,而不是刚发现一个对象便立即跳转过去扫描。
优化效果
Green Tea 将原本分散的对象扫描,转变为页内对象的批量连续扫描,从而:
- 提高内存访问的空间局部性;
- 提升 CPU 缓存命中率;
- 减少跨页、跨内存区域的频繁跳转;
- 分摊读取和处理对象元数据的成本;
- 缩小工作队列,降低多核并发访问工作队列时的竞争;
- 为 SIMD 向量化扫描提供更规整的数据布局。
可以用一句话总结两者的区别:
传统 GC 是“发现一个对象,就尽快扫描一个对象”;Green Tea 则是“先按页积累待扫描对象,再一次性批量扫描同一页中的多个对象”。