MENU

Golang Runtime 垃圾回收

September 15, 2021 • 学习

楔子

读《Go语言学习笔记》下卷

粗读一遍,记录下值得所关注以及记忆的内容。较少涉及汇编内容。

本文整理过程中可能包含了不同版本的源码,1.5.1、1.16。

GC

按官方说法,Go GC的基本特征是:非分代、非紧缩、写屏障、并发标记清理。

GC的时机很重要。太频繁会导致过于消耗资源、或者过早清理;太晚则会导致堆内存过度膨胀。

三色标记

三色标记法是传统 Mark-Sweep 的一个改进,它是一个并发的 GC 算法。

  1. 起初,所有对象均为白色
  2. 从根节点开始遍历所有对象(非递归遍历),把遍历到的对象从白色集合放入灰色集合(仅遍历根节点同级的对象,不进一步遍历)。
  3. 之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合。
  4. 重复,直到灰色中无任何对象。
  5. 通过写屏障检测对象有变化,重复以上操作
  6. 收集所有白色对象(垃圾)

三色标记出错

  • 条件1: 赋值器修改对象图,导致某一黑色对象引用白色对象;
  • 条件2: 从灰色对象出发,到达白色对象的、未经访问过的路径被赋值器破坏。

写屏障

在诸多屏障技术中,Go 使用了 Dijkstra 与 Yuasa 屏障的结合, 即混合写屏障(Hybrid write barrier)技术。 Go 在 1.8 的时候为了简化 GC 的流程,同时减少标记终止阶段的重扫成本, 将 Dijkstra 插入屏障和 Yuasa 删除屏障进行混合,形成混合写屏障,沿用至今。

如果说GC在STW阶段执行,那么三色标记法是合理的。但如果强调并行化,则在上述流程的第3步之后,若黑色对象引用了白色对象,那么三色标记法根本无法扫描到该白色对象,该白色对象会被错误地清理。

屏障上需要依赖多种操作来应对指针的插入和删除:

  • 扩大波面:将白色对象作色成灰色
  • 推进波面:扫描对象并将其着色为黑色
  • 后退波面:将黑色对象回退到灰色

在 Go 1.8 之前,为了减少写屏障的成本,Go 选择没有启用栈上写操作的写屏障, 赋值器总是可以通过将一个单一的指针移动到某个已经被扫描后的栈, 从而导致某个白色对象被标记为灰色进而隐藏到黑色对象之下,进而需要对栈的重新扫描, 甚至导致栈总是灰色的,因此需要 STW。

混合写屏障为了消除栈的重扫过程,因为一旦栈被扫描变为黑色,则它会继续保持黑色,并要求将对象分配为黑色。自Go1.8之后,栈上的对象一律标记成黑色,以保证栈运行效率。

这种情况下,垃圾回收器是增量而非并发的,但最终必须处理严格限制的世界时间的相同问题。

灰色对象的 Dijkstra 插入屏障

插入屏障技术,又称为增量更新屏障。 其核心思想是把赋值器对已存活的对象集合的插入行为通知给回收器,进而产生可能需要额外(重新)扫描的对象。 如果某一对象的引用被插入到已经被标记为黑色的对象中,这类屏障会保守地将其作为非白色存活对象, 以满足强三色不变性。

Dijkstra 插入屏障作为诸多插入屏障中的一种, 对于插入到黑色对象中的白色指针,无论其在未来是否会被赋值器删除,该屏障都会将其标记为可达(着色)。

该屏障提出时的基本思想是:对正在被覆盖的对象进行着色,且如果当前栈未扫描完成,则同样对指针进行着色。

黑色对象的 Yuasa 删除屏障

删除屏障技术,又称为基于起始快照的屏障。其思想是当赋值器从灰色或白色对象中删除白色指针时,通过写屏障将这一行为通知给并发执行的回收器。这一过程很像是在操纵对象图之前对图进行了一次快照。

如果一个指针位于波面之前,则删除屏障会保守地将目标对象标记为非白色存活对象,进而避免条件 2 来满足弱三色不变性。 具体来说,Yuasa 删除屏障在回收过程中,对于被赋值器删除最后一个指向这个对象导致该对象不可达的情况,仍将其对象进行着色。

批量写屏障缓存

在 Go 1.8 的实现中,如果无条件对引用双方进行着色,自然结合了 Dijkstra 和 Yuasa 写屏障的优势,但缺点也非常明显,因为着色成本是双倍的,而且编译器需要插入的代码也成倍增加,随之带来的结果就是编译后的二进制文件大小也进一步增加。为了针对写屏障的性能进行优化,Go 1.10 和 Go 1.11 中,Go 实现了批量写屏障机制。其基本想法是将需要着色的指针统一写入一个缓存, 每当缓存满时统一对缓存中的所有 ptr 指针进行着色。

说明

GC有触发阈值,阈值会随着每次内存使用变大而逐渐增大(如初始阈值是10MB则下一次就是 20MB,再下一次就成为了40MB),如果长时间没有触发GC,会主动触发一次(2min)。高峰时内存使用量上去后,除非持续申请内存,靠阈值触发gc已经基本不可能,而是要等最多2min主动GC开始才能触发GC。

Go语言在向系统交还内存时只是告诉系统这些内存不需要使用了,可以回收;同时操作系统会采取“拖延症”策略, 并不是立即回收,而是等到系统内存紧张时才会开始回收这样该程序又重新申请内存时就可以获得极快的分配速度。

表面上,指针参数的性能要更好一些,但是实际上具体分析,被复制的指针会延长目标对象的生命周期,还可能会导致对象被分配到堆上去,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。

Archives QR Code
QR Code for this page
Tipping QR Code