深入 Go 底层原理(六):垃圾回收(GC)
1. 引言
Go 语言自带垃圾回收(Garbage Collection, GC),让开发者从手动管理内存的繁重任务中解脱出来。Go 的 GC 以其低延迟和并发性而闻名,其目标是在不长时间暂停(Stop The World, STW)整个程序的情况下完成大部分回收工作。
本文将深入探讨 Go GC 的核心算法——并发三色标记清除法,以及其关键技术——写屏障(Write Barrier)。
2. GC 的目标与挑战
目标:回收堆上不再被使用的对象(垃圾),并将内存返还给分配器。
挑战:如何在不影响程序正常运行(即低 STW 时间)的前提下,准确、高效地完成回收?这需要在“mutator”(用户 Goroutine,会修改对象引用关系)和“collector”(GC Goroutine)之间进行精妙的协调。
3. 核心算法:并发三色标记-清除 (Tri-color Mark-and-Sweep)
Go GC 采用的是三色标记清除算法,它将堆上的对象分为三种颜色:
白色 (White):对象的初始状态,代表可能是垃圾。在 GC 周期结束时,所有仍然是白色的对象都将被回收。
灰色 (Grey):对象本身已被标记为存活,但其引用的其他对象(其子对象)还没有被扫描。灰色对象是待处理任务的集合。
黑色 (Black):对象本身和其引用的所有子对象都已被扫描,是确认的存活对象。
GC 流程分为四个主要阶段:
Mark Setup (STW):
这是一个短暂的 STW 阶段(通常在微秒级别)。
主要任务是开启写屏障 (Write Barrier),并准备标记工作。
将所有全局变量和每个 Goroutine 栈上的对象(根对象)放入灰色集合。
Marking (Concurrent):
这是 GC 的主要工作阶段,与用户 Goroutine 并发执行。
GC Goroutine 会从灰色集合中取出一个对象,将其标记为黑色。
然后扫描该对象的所有指针字段,将其引用的所有白色对象标记为灰色,并放入灰色集合。
这个过程会一直持续,直到灰色集合为空。
Mark Termination (STW):
这是另一个短暂的 STW 阶段。
主要任务是处理一些在并发标记阶段中被写屏障捕获的、可能被遗漏的指针修改,并关闭写屏障。
Sweeping (Concurrent):
此阶段也与用户 Goroutine 并发执行。
GC 会遍历堆中的所有
mspan
,回收所有仍然是白色对象的内存块,并将其返还给内存分配器。
4. 关键技术:写屏障 (Write Barrier)
在并发标记阶段,如果用户 Goroutine(mutator)修改了对象的引用关系,可能会破坏三色标记的不变性,导致本应存活的对象被错误回收。
危险场景:一个黑色对象引用了一个白色对象,同时该白色对象的所有其他灰色父引用被移除了。如果不加干预,这个白色对象将永远不会被扫描,最终被当成垃圾回收。
黑色对象 -> 白色对象
为了防止这种情况,Go 引入了写屏障。写屏障是编译器插入的一小段代码,它会“拦截”所有在堆上的指针写操作。
混合写屏障 (Hybrid Write Barrier, Go 1.8+): Go 的混合写屏障结合了两种屏障的优点,其核心思想是:
它保护的是白色对象:不允许黑色对象直接引用白色对象。
工作机制:当
*slot = ptr
(一个指针写操作)发生时,如果ptr
指向一个白色对象,写屏障会将ptr
指向的对象涂成灰色。
通过这种方式,任何可能被黑色对象引用的白色对象都会被“拯救”回来,加入灰色集合,从而保证了 GC 的正确性。
5. GC 的触发时机
GC 主要由以下条件触发:
内存分配阈值 (
GOGC
): 当自上次 GC 以来新分配的内存达到一个阈值时,会自动触发新的 GC。这个阈值由环境变量GOGC
控制(默认为 100),表示当堆大小增长 100% 时触发。定时触发:
runtime.sysmon
线程会定期检查,如果距离上次 GC 超过一定时间(默认为 2 分钟),会强制触发一次 GC。手动触发: 开发者可以调用
runtime.GC()
来手动触发一次 GC。
6. 总结
Go GC 是一个低延迟、高并发的垃圾回收系统。它通过三色标记清除算法实现了大部分工作的并发执行,通过短暂的 STW 完成必要的同步,并通过混合写屏障技术保证了在用户 Goroutine 并发修改对象引用时的正确性。这一系列精巧的设计,是 Go 能够胜任高并发、低延迟服务场景的重要保障。