Go GC机制浅析
Go 采用的是 三色并发标记清除算法。
传统的 GC 算法需要较长的 STW (Stop The World) 停顿时间(即暂停所有应用程序代码的运行来执行 GC)。
Go GC 的首要目标就是将 STW 停顿时间降到毫秒级甚至微秒级。
三色标记法
Go GC将堆上的所有对象分为三种颜色:
| 颜色 | 状态 | 描述 |
|---|---|---|
| Black | 已扫描存活 | 自身及其引用的所有对象都已被检查,确定为存活对象。黑色对象在本次 GC 周期内不会再被扫描。 |
| White | 未访问或待清除 | 尚未被 GC 访问,如果 GC 结束后仍是白色,则会被判定为垃圾并清除。 |
| Gray | 待扫描 | 自身已被标记为存活,但它引用的对象(子对象)尚未被扫描。 |
工作流程:
- 初始状态: 所有的堆对象都是白色。
- 根对象扫描: GC 从根对象(包括全局变量、活跃的 Goroutine 栈上的变量等)开始,将它们标记为灰色。
- 遍历扩散: GC 不断从灰色集合中取出一个对象:
- 将其引用的所有白色子对象标记为灰色。
- 然后将该对象自身标记为黑色。
- 循环结束: 当灰色集合为空时,所有存活对象都已标记为黑色或灰色(如果不是根对象,灰色对象在下一轮会被标记为黑色),剩下的白色对象就是垃圾。
并发
Go GC 的大部分标记工作都是与用户程序并发进行的。这大大减少了 STW 停顿时间。
- 实现: GC 作为一个或多个特殊的 Goroutine 运行,与用户程序共享 CPU 资源。
写屏障
并发带来的最大挑战是:当 GC 在标记对象时,用户程序可能会更改指针引用,导致 GC 出现错误标记。
错误标记的两种可能:
| 错误类型 | 发生场景 | 结果 |
|---|---|---|
| 丢失标记 | 应用程序在标记过程中将白色对象连接到了黑色对象,同时切断了该白色对象与灰色对象的唯一联系。 | 该存活对象被误判为垃圾,最终被清除(程序崩溃!)。 |
| **重复标记 ** | 应用程序将一个 白色对象 连接到了 灰色对象。 | 影响效率,但不会导致错误。 |
为了解决丢失标记这一致命问题,Go 在 1.8 版本后采用了 混合写屏障。其核心思想是:
- 在指针写入前(Pre-Write):如果当前要被覆盖的指针指向一个对象
A,则将对象A标记为灰色(即保证其被扫描)。 - 栈扫描时 STW: 在 GC 过程中,栈上的对象处理(扫描)必须在 STW 期间进行(非常短),以避免写屏障对栈操作的影响。
** 写屏障保证了在并发标记期间,所有存活的对象**都不会被误标为白色并清除。
GC Pacing
Go GC 并非盲目地等待内存不足才运行,而是通过 Pacer 机制来规划运行时间,以平衡性能和内存消耗。
- GOGC 变量: 环境变量
GOGC控制了目标堆大小。默认值是100。 - 触发目标: 当当前堆内存使用量达到上次 GC 完成后存活对象大小的
(1 + GOGC/100)倍时,GC 就会开始。- 例如,如果 GOGC=100,上次活对象是 10MB,那么当堆内存增长到 20MB 时,GC 就会启动。
GC Cycle
一个完整的 GC 循环
-
标记启动:暂停所有 Goroutine;开启写屏障,设置 GC 状态。
-
并发标记
-
并发执行: GC Goroutine 开始扫描堆,执行三色标记,同时用户程序也在运行。
-
写屏障介入: 应用程序的所有指针操作都受到写屏障的监控。
-
-
标记终止
-
STW 暂停所有 Goroutine,进行最后的清理和同步,包括扫描栈上的对象以确保万无一失。
-
关闭: 关闭写屏障,确认所有存活对象已标记。
-
-
并发清除:GC 遍历整个堆内存,回收所有未被标记(白色)的对象,并将空闲的内存块合并到空闲链表中,供下次分配使用。