垃圾回收gc

Go V1.3标记-清除算法

具体过程

第一步:暂停程序(STW)业务逻辑, 分类出可达和不可达的对象,然后做上标记

第二步, 开始标记,程序找出它所有可达的对象,并做上标记

第三步, 标记完了之后,然后开始清除未标记的对象。

STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大

第四步, 停止暂停,让程序继续跑。然后循环重复这个过程,直到process程序生命周期结束。

缺点

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

Go V1.5三色并发标记法

并发运行需要一定时间的STW

具体过程

第一步 , 每次新创建的对象,默认的颜色都是标记为“白色”;

第二步, 每次GC回收开始, 会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入“灰色”集合。

遍历是一次遍历可抵达的对象遍历一层

第三步, 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合。

这一次遍历是只扫描灰色对象,将灰色对象的第一层遍历可抵达的对象由白色变为灰色,如:对象2、对象7. 而之前的灰色对象1和对象4则会被标记为黑色,同时由灰色标记表移动到黑色标记表中。

第四步, 重复第三步, 直到灰色中无任何对象,如图所示。

当我们全部的可达对象都遍历完后,灰色标记表将不再存在灰色对象,目前全部内存的数据只有两种颜色,黑色和白色。那么黑色对象就是我们程序逻辑可达(需要的)对象,这些数据是目前支撑程序正常业务运行的,是合法的有用数据,不可删除,白色的对象是全部不可达对象,目前程序逻辑并不依赖他们,那么白色对象就是内存中目前的垃圾数据,需要被清除。

第五步: 回收所有的白色标记表的对象,也就是回收垃圾。

以上我们将全部的白色对象进行删除回收,剩下的就是全部依赖的黑色对象。

在开始三色标记之前就会加上STW``,``在扫描确定黑白对象之后再放开STW

没有STW的三色标记法

他是一定要依赖STW的

因为如果不暂停程序, 程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改,会影响标记结果的正确性。

本来是对象4合法引用的对象3,却被GC给“误杀”回收掉了。

有两种情况,在三色标记法中,是不希望被发生的。

●条件1: 一个白色对象被黑色对象引用(白色被挂在黑色下)

●条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色) 如果当以上两个条件同时满足时,就会出现对象丢失现象!

并且,如图所示的场景中,如果示例中的白色对象3还有很多

为了防止这种现象的发生,最简单的方式就是STW,直接禁止掉其他用户程序对对象引用关系的干扰,但是STW的过程有明显的资源浪费,对所有的用户程序都有很大影响。

屏障机制

(1) “强-弱” 三色不变式

●强三色不变式

不存在黑色对象引用到白色对象的指针。

弱三色不变色实际上是强制性的不允许黑色对象引用白色对象,这样就不会出现有白色对象被误删的情况。

●弱三色不变式

所有被黑色对象引用的白色对象都处于灰色保护状态。

弱三色不变式强调,黑色对象可以引用白色对象,但是这个白色对象必须存在其他灰色对象对它的引用,或者可达它的链路上游存在灰色对象。 这样实则是黑色对象引用白色对象,白色对象处于一个危险被删除的状态,但是上游灰色对象的引用,可以保护该白色对象,使其安全。

为了遵循上述的两个方式,GC算法演进到两种屏障方式,他们“插入屏障”, “删除屏障”。

(2)  插入屏障

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

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

伪码如下:

添加下游对象(当前下游对象slot, 新下游对象ptr) { 

//1

标记灰色(新下游对象ptr) 

//2

当前下游对象slot = 新下游对象ptr                   

}

场景:

A.添加下游对象(nil, B)   //A 之前没有下游, 新添加一个下游对象B, B被标记为灰色

A.添加下游对象(C, B)     //A 将下游对象C 更换为B,  B被标记为灰色

这段伪码逻辑就是写屏障,. 我们知道,黑色对象的内存槽有两种位置, 栈和堆. 栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用, 所以“插入屏障”机制,在栈空间的对象操作中不使用. 而仅仅使用在堆空间对象的操作中.

接下来,我们用几张图,来模拟整个一个详细的过程, 希望您能够更可观的看清晰整体流程。

但是如果栈不添加,当全部三色标记扫描之后,栈上有可能依然存在白色对象被引用的情况(如上图的对象9). 所以要对栈重新进行三色标记扫描, 但这次为了对象不丢失, 要对本次标记扫描启动STW暂停. 直到栈空间的三色标记结束.

最后将栈和堆空间 扫描剩余的全部 白色节点清除.  这次STW大约的时间在10~100ms间.

(3) 删除屏障

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

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

伪代码:

添加下游对象(当前下游对象slot, 新下游对象ptr) {
  //1
  if (当前下游对象slot是灰色 || 当前下游对象slot是白色) {
      标记灰色(当前下游对象slot)     //slot为被删除对象, 标记为灰色
  }
  
  //2
  当前下游对象slot = 新下游对象ptr
}

场景:

A.添加下游对象(B, nil)   //A对象,删除B对象的引用。  B被A删除,被标记为灰(如果B之前为白)
A.添加下游对象(B, C)     //A对象,更换下游B变成C。   B被A删除,被标记为灰(如果B之前为白)

接下来,我们用几张图,来模拟整个一个详细的过程, 希望您能够更可观的看清晰整体流程。

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

Go V1.8混合写屏障机制

插入写屏障和删除写屏障的短板:

● 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;

● 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。

Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。

(1) 混合写屏障规则

具体操作:

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

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

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

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

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

伪代码:

添加下游对象(当前下游对象slot, 新下游对象ptr) {
    //1 
    标记灰色(当前下游对象slot)    //只要当前下游对象被移走,就标记灰色
    
    //2 
    标记灰色(新下游对象ptr)
      
    //3
    当前下游对象slot = 新下游对象ptr
}

这里我们注意, 屏障技术是不在栈上应用的,因为要保证栈的运行效率。

(2) 混合写屏障的具体场景分析

接下来,我们用几张图,来模拟整个一个详细的过程, 希望您能够更可观的看清晰整体流程。

注意混合写屏障是Gc的一种屏障机制,所以只是当程序执行GC的时候,才会触发这种机制。

GC开始:扫描栈区,将可达对象全部标记为黑

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

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

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

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

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

总结

GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。

GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通

GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。

普通标记清除法(STW,产生堆碎片)

  1. 先STW(停止所有业务),分类出可达和不可达的对象,然后做上标记;
  2. 开始标记,程序找出它所有可达的对象,并做上标记。
  3. 标记完了之后,然后开始清除未标记的对象.。

golang三色标记法(并发,SWT时间短)

golang采用的是三色标记法,属于追踪算法的一种。过程如下:(颜色:白→灰→黑)

  1. 刚开始,所有堆和栈上的对象全部标记为白色
  2. 从根节点开始,对所有有引用其他对象的对象标记(可达对象,不使用递归)为灰色
  3. 遍历所有灰色对象,将有引用到的子对象标记为灰色,同时自身标记为黑色
  4. 循环第三步,直到灰色对象中没有多余对象
  5. 清理白色标记的对象

golang混合写屏障实现(无STW,占空间省时间)

堆上应用

golang gc流程

  1. STW(StopTheWorld)开启写屏障,记录数据段以及栈中roots的必要信息
  2. StartTheWorld开始标记,此时mutator(用户程序)和GC标记并发执行
  3. 标记完成再次STW,关闭写屏障
  4. StartTheWorld进入轻扫阶段

垃圾回收算法,“停止世界”(Stop the world:STW)是跟踪内存使用最重要的阶段,它会停止程序的执行,以扫描内存使用,并添加写屏障。

  • 追踪式垃圾回收算法
  • 引用计数法(Reference counting)

参考