Golang 的垃圾回收

三色标记算法

三色标记算法改进了标记-清除算法,将标记-清除算法的两个阶段(标记和清除)分解为三个阶段(标记、标记终止和清除),减少了 STW 的时间。

三种对象

颜色
对象状态
描述

白色

未访问

对象未被访问, 可能是需要回收的对象

灰色

访问中

对象已被访问,但其子对象未被访问

黑色

访问完成

对象已被访问,且其子对象已被访问

最终回收的是白色的对象。

工作原理

  1. 在垃圾回收开始时将根对象标记为灰色

  2. 在灰色对象中选择一个对象标记为黑色,然后将其子对象标记为灰色

  3. 将黑色对象指向的所有白色对象标记为灰色

  4. 重复步骤2和3,直到没有灰色对象

  5. 清除所有白色对象

三色标记法过程
三色标记法过程

假如不 STW 会怎样?

实际上,如果正常按照三色标记法进行 STW 的话, STW 的时间仍旧比较长。但是如果不 STW,那么在标记和清除的过程中,程序可能会继续运行,这样可能会导致对象的状态发生变化,从而导致垃圾回收器无法正确标记对象的状态,最终导致回收错误。

GC Error
GC Error

如上图所示,假如遍历完 AD 之后,在遍历到达 B 之前,若 D 添加了对 C 的引用, B 移除了 C 的引用, 则 C 将会在 GC 之后变为白色,会被垃圾回收。

屏障技术

为了解决上述问题, Golang 引入了屏障技术,通过屏障技术可以在对象状态发生变化时,通知垃圾回收器。

重要

若我们希望在并发或增量标记算法中保证标记的正确性,我们需要达成以下其中一种三色不变性:

  • 强三色不变性:在标记阶段中,黑色对象不会指向白色对象

  • 弱三色不变性:在标记阶段中,黑色对象指向的白色对象(G)必须包含一条灰色对象经过一个或多个白色对象后到达白色对象(G)的路径

弱三色不变性
弱三色不变性

如上图所示,假如 A 添加了对 D 的引用,则需要再 E 添加指向 D 的引用,这样才能保证弱三色不变性。

Golang 用到的屏障技术

  • 插入屏障

  • 删除屏障

插入屏障

Golang 中,当一个对象 A 添加了对另一个对象 B 的引用时,会在 A 的引用列表中插入一个 B 的引用,并且将 B 标记为灰色。

[!warning] 插入屏障只会在堆内生效,不会在栈内生效,主要考虑到性能问题

插入屏障
插入屏障

如上图所示,在初始条件下(图1), A 属于栈内数据,F 属于堆内数据,在图2 中同时往 A 添加 D 的引用, 往 F 添加 H 的引用。 H 由于插入屏障会变成灰色,而 D 由于不在堆内,不会变成灰色。当扫描完毕时,如图4 所示,H 会被标记为黑色,而 D 会被标记为白色。这时候会启动 STW 将栈内对象重新扫描一遍,将 D 标记为黑色。

删除屏障

Golang 中,当一个对象 A 删除了对另一个对象 B 的引用时,会在 A 的引用列表中删除一个 B 的引用,如果 B 是白色的,则将 B 标记为灰色。

删除屏障
删除屏障

之所以将白色的对象标记为灰色,是因为白色的对象后面可能还有其他对象引用,如果不标记为灰色,可能会导致后续的对象无法被扫描到。

混合写屏障

插入屏障和删除屏障有以下缺点:

  • 插入屏障在扫描结束后还需要 STW 一次,将栈内对象重新扫描一遍

  • 删除屏障回收精度较低,在回收开始时需要 STW 一次,将栈内对象重新扫描一遍, 记录初始快照,保护初始时刻所有存活的对象

为了解决上述问题, Golang 引入了混合写屏障,混合写屏障是插入屏障和删除屏障的结合,可以在对象状态发生变化时,通知垃圾回收器。

工作原理

  1. 在垃圾回收开始时将栈上的对象全部扫描并标记为黑色(不进行二次扫描)

  2. 在垃圾回收期间任何栈上创建的对象都会标记为黑色,避免了二次扫描

  3. 在垃圾回收期间删除任何的对象都会标记为灰色

  4. 在垃圾回收期间创建的任何对象都会标记为灰色

最后更新于