V1.3前-标记清除法

mark and sweep

GoV1.3之前的标记清除: mark and sweep

  1. 暂停程序业务逻辑STW(stop the world), 分类出可达和不可达的对象,然后做上标记。
  2. 程序找出它所有可达的对象,并做上标记。
  3. 清除未标记的对象。(对象5,6不可达,被GC清除)
  4. 停止暂停,让程序继续跑。然后循环重复这个过程,直到process程序生命周期结束。

注意:mark and sweep算法在执行的时候,需要程序暂停!即 STW(stop the world),STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,所以STW也是一些回收机制最大的难题和希望优化的点。所以在执行第三步的这段时间,程序会暂定停止任何工作,卡在那等待回收执行完毕。

mark and sweep的缺点:

  1. STW,stop the world;让程序暂停,程序出现卡顿 (重要)
  2. 标记需要扫描整个heap(堆);
  3. 清除数据会产生heap碎片。

V1.3 STW步骤提前

STW的步骤提前了一步,因为在Sweep清除的时候,可以不需要STW停止,因为这些对象已经是不可达对象了,不会出现回收写冲突等问题。

但是无论怎么优化,Go V1.3都面临这个一个重要问题,就是mark-and-sweep 算法会暂停整个程序

V1.5 三色标记法

  1. 每次新创建的对象,默认的颜色都是标记为“白色”
  2. 每次GC回收开始, 会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入“灰色”集合
  3. 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
  4. 重复第三步, 直到灰色中无任何对象
  5. 回收所有的白色标记表的对象. 也就是回收垃圾

没有STW的三色标记法

在还没有扫描到对象2的时候,如果对象4指向对象3,同时对象2指针p移除,会导致对象3被误杀。

对象丢失

  • 条件1: 一个白色对象被黑色对象引用(白色被挂在黑色下)
  • 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色)

当以上两个条件同时满足时,就会出现对象丢失现象!

强弱三色不变式

GC回收器满足下面两种情况之一时,即可保对象不丢失。 这两种方式就是“强三色不变式”和“ 弱三色不变式”。

  • 强三色不变式:不存在黑色对象引用到白色对象的指针。
  • 弱三色不变式:所有被黑色对象引用的白色对象都处于灰色保护状态。

插入屏障

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

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

插入屏障机制,在栈空间的对象操作中不使用, 仅仅使用在堆空间对象的操作中。

处理流程:

  1. 程序创建,对象全部标记为白色,放入白色集合中
  2. 遍历Root Set(只遍历一次),得到灰色节点
  3. 遍历灰色标记表,将可达对象标记为灰色,遍历后的灰色对象标记为黑色
  4. 此时对象4添加对象8(堆区:触发插入屏障),对象1添加对象9(不触发)
  5. 由于插入写屏障,对象8变为灰色,对象9仍为白色

  1. 继续循环上述流程进行三色标记,直到没有灰色节点
  2. 在准备回收白色节点前,重新遍历扫描一次栈空间,此时加STW暂停保护栈,防止外界干扰(有新的白色被黑色添加)
  3. 在STW中,将占中的对象进行三色标记,直到没有灰色节点
  4. 停止STW
  5. 清除白色

不足:结束时需要STW来重新扫描,大约需要10~100ms

删除屏障

具体操作: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。

满足: 弱三色不变式. (保护灰色对象到白色对象的路径不会断)

处理流程:

  1. 程序创建,对象全部标记为白色,放入白色集合中
  2. 遍历Root Set(只遍历一次),得到灰色节点
  3. 对象1删除对象5,触发删除写屏障,对象5标记为灰色
  4. 循环进行三色标记,直到没有灰色节点
  5. 清除白色

缺点:回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。

V1.8 三色标记法+混合写屏障

混合写屏障

具体操作:

1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),

2、GC期间,任何在栈上创建的新对象,均为黑色。

3、被删除的对象标记为灰色。

4、被添加的对象标记为灰色。

满足: 变形的弱三色不变式

典型场景

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

//前提:堆对象4->对象7 = 对象7;  //对象7 被 对象4引用
栈对象1->对象7 = 堆对象7;  //将堆对象7 挂在 栈对象1 下游
堆对象4->对象7 = null;    //对象4 删除引用 对象7

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

new 栈对象9;
对象8->对象3 = 对象3;      //将栈对象3 挂在 栈对象9 下游
对象2->对象3 = null;      //对象2 删除引用 对象3

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

堆对象10->对象7 = 堆对象7;       //将堆对象7 挂在 堆对象10 下游
堆对象4->对象7 = null;         //对象4 删除引用 对象7

场景四:对象从一个栈对象删除引用,成为另一个堆对象的下游

堆对象10->对象7 = 堆对象7;       //将堆对象7 挂在 堆对象10 下游
堆对象4->对象7 = null;         //对象4 删除引用 对象7

Golang中的混合写屏障满足弱三色不变式,结合了删除写屏障和插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。

总结

  • GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。
  • GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通
  • GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。

Reference

Golang修养之路


IT小马
1.2k 声望166 粉丝

Php - Go - Vue - 云原生