golang垃圾回收

golang的垃圾回收的发展主要有三个阶段

  1. golangv1.3之前的标记-清除法(Mark and Sweep)
  2. golangv1.5的三色并发标记法(强-弱三色不变式, 插入、删除屏障)
  3. golangv1.8的混合写屏障

标记-清除法(Mark and Sweep)

主要包含两个阶段

  • 标记
  • 清除

具体步骤

  1. STW(stop the world),暂停所有程序,分出可达类和不可达类
  2. 标记所有可达类节点
  3. 清除所有不可达类节点
  4. 停止暂停,让程序继续跑。然后循环重复这个过程,直到程序生命周期结束。

优势

非常简单,STW暂停所有的程序运行,清理所有不可达对象。

劣势

简单的代价就是在清理垃圾的过程中,所有程序暂停,程序出现卡顿 问题(STW带来的后果

image-20250922143345654

gov1.3再做了简单的优化,因为不可达对象都是程序不需要的对象,所以可以把STW提前,减少暂停的时间

image-20250922144501423

如何提高垃圾回收的效率?很直观的想法就是降低STW的时间,后续就有了一系列优化方案。

三色并发标记法

golangV1.5推出了三色并发标记法

具体步骤

  • 初始阶段都是白色对象
  • 遍历白色对象,把可达对象变成灰色
  • 遍历灰色对象,把可达对象变成黑色
  • 剩下的白色对象就是不可达对象(垃圾),需要进行清除

染色思路是很清晰的 ,但是在gc的开始三色标记之前加上 STW,在扫描确定黑白对象之后再放开 STW。所以程序还是会有卡顿现象。

为什么三色并发标记法需要STW

假设在三色并发标记的过程中没有STW

理想情况下,扫描任务继续执行,对象2、3、5最终都变成黑色节点,把对象6清除掉。

image-20250922150331907

但是!由于没有STW,所以任意的对象均可能发生读写操作。在本次扫描开始之前,对象2把对象3的引用删除了,对象4加上了对象3的引用

image-20250922151444585

在扫描过程中,因为对象2删除了对象3的引用,所以对象3不会变成灰色;对象4已经变成黑色了,所以不会继续遍历黑色对象。最终,对象3变成了一个垃圾被清除了,程序2因为需要对象3,但是他被回收掉了,所以可能会引发程序崩溃等等现象。

屏障机制

其实在理想情况下,不启用STW是可行的,但一旦出现

  1. 黑色对象新增白色对象的引用
  2. 可达的白色对象被删除

上面两种情况,就有可能会出现对象丢失的情况

为了避免出现上面两种情况的发生,出现了强-弱三色不变式两种方式

强三色不变式

强三色不变式约定了,黑色对象不能引用白色对象

弱三色不变式

弱三色不变式约定了,黑色对象只可以引用可达路径上的白色对象

强弱三色不变式约定了新增和删除对象时,需要对对象状态进行判断,gc就进而演化出插入屏障和删除屏障。

插入屏障

具体操作: 在 A 对象引用 B 对象的时候,B 对象被标记为灰色。(将 B 挂在 A 下游,B 必须被标记为灰色)

满足: 强三色不变式. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)

但是。对象内存分配有可能在或者上,栈的特点是速度快,但是存储空间小。堆的特定是速度慢,但是存储空间大。因为栈上会操作频率很快,所以插入屏障都是作用于堆上的。堆上扫描完后,通过STW重新堆栈上对象扫描。

插入屏障的工作范围
  • 堆上引用:触发插入屏障,被引用对象标记为灰色
  • 栈上引用:不触发插入屏障,保持原有颜色

例子:

  1. 堆上新增一个白色对象引用,触发插入屏障,新引用的对象直接会被标记为灰色
  2. 栈上新增一个白色对象引用,不会触发插入屏障,还是白色对象。但是最终会重新对栈上的对象进行STW扫描,原本的白色对象在可达链路上,最终会变成黑色。
栈重扫描的必要性

插入屏障只作用于堆上对象,这会导致一个问题:

问题场景

1
栈上白色对象A ← 堆上黑色对象B引用
  1. 堆上对象B引用栈上对象A,但不触发插入屏障
  2. 对象A保持白色状态
  3. GC扫描时,A会被误判为垃圾

解决方案
插入屏障必须配合栈重扫描:

  1. 标记阶段结束后,STW开始
  2. 重新遍历所有goroutine的栈帧
  3. 检查栈上每个对象的引用状态
  4. 将被引用的白色对象重新标记为灰色
  5. 继续标记直到没有灰色对象

栈重扫描的局限性
栈重扫描只能发现栈上变量直接引用的对象,对于”堆引用栈且栈上无其他引用”的场景仍存在漏检风险。实际实现中通过逃逸分析和保守扫描来解决。

优势
  1. 满足三色不变式
  2. 减少STW时间(相比传统标记清除)
劣势
  1. 结束时需要 STW 来重新扫描栈
  2. 栈重扫描增加了STW时间
  3. 存在栈-堆引用关系的复杂处理问题

删除屏障

具体操作: 在 A 对象删除 B 对象引用的时候,如果B本身就是灰色或者白色对象,则直接标记为灰色

满足: 弱三色不变式(保护可达路径上的白色对象不被删除)

image-20250922195046719

比如删除对象1对对象5的引用,对象5会被标记为灰色,最终对象2、3、5都会变成黑色。虽然对象2、3、5都是黑色节点,但是如果没有任何对象引用对象2、3、5的话,这三个节点会在下一次gc的时候被清除。

优势
  1. 简单,维护成本低
劣势
  1. 精确性没那么高

混合写屏障机制

插入屏障精确,但是需要通过STW重新扫描栈上的对象,删除屏障性能高,但是没那么精确,部分对象引用需要下一次gc才能处理。所以golangV1.8使用了混合写屏障,在精确性和性能之间找到最佳平衡点。

具体步骤

  1. GC 开始将栈上的对象全部扫描并标记为黑色
  2. GC期间,栈上创建的新对象,均为黑色
  3. 堆上被删除的对象标记为灰色
  4. 堆上被添加的对象标记为灰色

场景一:对象被堆上删除,成为栈对象的下游

image-20250923113255722

  1. 栈上对象1不启动写凭证,所以可以直接引用对象7
  2. 对象4删除对象7的引用,触发删除屏障,对象7会变成灰色
  3. 最终gc回收对象5、6、8

场景二:对象被一个栈对象删除引用,成为另一个栈对象的下游

image-20250923113458219

image-20250923113542837

  1. 栈上对象9引用对象3,栈上没有写屏障可以直接引用
  2. 栈上对象2删除对象3的引用,栈上没有写屏障可以直接删除
  3. 最终gc会回收对象5、6、8

场景三:对象被一个堆对象删除引用,成为另一个堆对象的下游

image-20250923142256233

image-20250923142015617

  1. 堆上对象10引用堆上对象7,触发写屏障,对象7会变成灰色
  2. 对象4删除对象7的引用,对象7还是变成灰色
  3. 最终gc回收对象5、8、11

场景四:对象被栈上删除,被堆上对象引用

image-20250923142551829

  1. 栈上对象1删除对象2的引用,不触发屏障,可以直接删除
  2. 堆上对象4引用栈上对象2,删除对象7的引用。删除对象7的引用触发屏障,对象7会变成灰色;
  3. 最终回收对象5、8、11;如果下一轮gc对象7还没有被引用,会继续被清除