Go 语言的垃圾回收机制概览
3月 22, 2021
之前已经看过了 常见的垃圾回收算法,有了一定的基础,现在再来了解下 Go 语言的垃圾回收机制。
三色抽象 #
Go 语言如今已经演变成增量式的垃圾回收策略,即不是一口气运行 GC,而是和 Mutator 交替运行的,因此不会长时间妨碍到 Mutator 的运行。Go 语言增量式回收是基于“标记-清除”算法的一种优化思路,具体的实现是三色标记法。
简单来说,三色标记算法将程序中的对象分成白色、黑色和灰色三类:
- 白色对象 — 潜在的垃圾,其内存可能会被垃圾收集器回收;
- 黑色对象 — 活跃的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象;
- 灰色对象 — 活跃的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;
在垃圾收集器开始工作时,程序中不存在任何的黑色对象,垃圾收集的根对象会被标记成灰色,从根对象开始扫描并标记成黑色,当灰色集合中不存在任何对象时,标记阶段就会结束。
一轮标记之后,应用程序的堆中就只有标记到的黑色和剩余的白色对象,垃圾收集器会回收所有的白色对象并将所有的黑色对象重新标记为灰色,再次重复遍历灰色根对象阶段。
白色对象集合的回收是存在一定复杂度的,由于在标记阶段中已经确定对象的颜色后,在真正回收之前,用户程序可以再次对某个白色对象进行引用,此时再对此白色对象进行回收就会是错误行为,从而影响内存的安全性,所以想要并发或者增量地标记对象还是需要使用写入屏障技术。
屏障技术 #
屏障技术相当于是钩子函数,它是在用户程序读取对象、更新对象、创建对象指针时执行的一段代码,根据操作类型不同又分为读屏障和写屏障,该技术能够保证内存操作的顺序性。由于在读操作远远高于写操作,所以为了性能,都是以写屏障来保证两种三色不变性:
- 强三色不变形:黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象。
- 弱三色不变形:黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径。
通过屏障技术,用户程序和垃圾收集器可以在交替工作的情况下保证程序执行的正确性。Go 语言在 v1.8 组合 Dijkstra 插入写屏障和 Yuasa 删除写屏障构成了混合写屏障,该写屏障会将被覆盖的对象标记成灰色并在当前栈没有扫描时将新对象也标记成灰色:
writePointer(slot, ptr):
shade(*slot)
if current stack is grey:
shade(ptr)
*slot = ptr
为了移除栈的重扫描过程,除了引入混合写屏障之外,在垃圾收集的标记阶段,还需要将创建的所有新对象都标记成黑色,防止新分配的栈内存和堆内存中的对象被错误地回收,因为栈内存在标记阶段最终都会变为黑色,所以不再需要重新扫描栈空间。
执行周期 #
Go 语言的垃圾收集可以分成清除终止、标记、标记终止和清除四个不同阶段,它们分别完成了不同的工作。
清除终止 #
- 暂停程序,所有的处理器在这时会进入安全点(Safe point);
- 如果当前垃圾收集循环是强制触发的,我们还需要处理还未被清理的内存管理单元;
标记 #
- 将状态切换至
_GCmark
、开启写屏障、用户程序协助(Mutator Assists)并将根对象入队; - 恢复执行程序,标记进程和用于协助的用户程序会开始并发标记内存中的对象,写屏障会将被覆盖的指针和新指针都标记成灰色,而所有新创建的对象都会被直接标记成黑色;
- 开始扫描根对象,包括所有 Goroutine 的栈、全局对象以及不在堆中的运行时数据结构,扫描 Goroutine 栈期间会暂停当前处理器;
- 依次处理灰色队列中的对象,将对象标记成黑色并将它们指向的对象标记成灰色;
- 使用分布式的终止算法检查剩余的工作,发现标记阶段完成后进入标记终止阶段;
标记终止 #
- 暂停程序、将状态切换至
_GCmarktermination
并关闭辅助标记的用户程序; - 清理处理器上的线程缓存;
清除 #
- 将状态切换至
_GCoff
开始清理阶段,初始化清理状态并关闭写屏障; - 恢复用户程序,所有新创建的对象会标记成白色;
- 后台并发清理所有的内存管理单元,当 Goroutine 申请新的内存管理单元时就会触发清理;
触发时机 #
后台触发 #
运行时会在应用程序启动时在后台开启一个用于强制触发垃圾收集的 Goroutine,该 Goroutine 的职责非常简单 — 调用 runtime.gcStart
尝试启动新一轮的垃圾收集。
手动触发 #
用户程序会通过 runtime.GC
函数在程序运行期间主动通知运行时执行,该方法在调用时会阻塞调用方直到当前垃圾收集循环完成,在垃圾收集期间也可能会通过 STW 暂停整个程序。
申请内存时触发 #
最后一个可能会触发垃圾收集的就是 runtime.mallocgc
了,上一节内存分配器中曾经介绍过运行时会将堆上的对象按大小分成微对象、小对象和大对象三类,这三类对象的创建都可能会触发新的垃圾收集循环。
参考资料 #
源码级的探究可以继续参考 Go 语言设计与实现 里的第七章 内存管理篇 的内容,关于垃圾回收是个很有研究价值的话题,后续再继续深入学习。