Go的三色标记GC

三色标记

三色标记的原理以下:
整个进程空间里申请每一个对象占据的内存能够视为一个图, 初始状态下每一个内存对象都是白色标记,先stop the world,将扫描任务做为多个并发的goroutine当即入队给调度器,进而被CPU处理,第一轮先扫描全部可达的内存对象,标记为灰色放入队列;第二轮能够恢复start the world,将第一步队列中的对象引用的对象置为灰色加入队列,一个对象引用的全部对象都置灰并加入队列后,这个对象才能置为黑色并从队列之中取出。循环往复,最后队列为空时,整个图剩下的白色内存空间即不可到达的对象,即没有被引用的对象; 第三轮再次stop the world,将第二轮过程当中新增对象申请的内存进行标记(灰色),这里使用了writebarrier(写屏障)去记录这些内存的身份;golang

整个gc的流程以下图:并发

clipboard.png

注意到:
mark 有两个过程。优化

首先从 root 开始遍历,root 包括全局指针和 goroutine 栈上的指针,标记为灰色。遍历灰色队列。spa

re-scan 全局指针和栈。由于 mark 和用户程序是并行的,因此在上一步执行的时候可能会有新的对象分配,这个时候就须要经过写屏障(write barrier)记录下来。re-scan 再完成检查一下。指针

Stop The World 有两个过程。协程

第一个是 GC 将要开始的时候,这个时候主要是一些准备工做,好比 enable write barrier。对象

第二个过程就是上面提到的 re-scan 过程。若是这个时候没有 stw,那么 mark 将无休止。blog

mark完毕后start the world进行并行清理,对于并行清理,GC 初始化的时候就会启动 bgsweep()这个协程并一直在后台阻塞, 开始清理时将这个协程唤醒并给主M去作并发的sweep。队列

内存管理都是基于 span 的,mheap_ 是一个全局的变量,全部分配的对象都会记录在 mheap_ 中。在标记的时候,咱们只要找到对对象对应的 span 进行标记,清扫的时候扫描 span,没有标记的 span 就能够回收了。进程

另外:1.8之后的golang将第一步的stop the world 也取消了,这又是一次优化:

clipboard.png

写屏障

关于写屏障的用处 以下面的例子,这个例子修改自知乎上的一个问答,在此表示感谢:

GC前:
stack->a->b ; a为栈中申请的对象,b为堆中申请的对象,a对象中存在对b的引用;

stack->c ; c 也是栈中申请的对象。

stop the world, mark。 这里a,c都会被标记为灰色;b为白色
start the world 反复mark。
因为是并发的mark,咱们假设c先被处理,c没有引用其余对象,因此直接置黑,从队列中取出;此时c为黑色,a为灰色,b为白色

假设这时用户作了以下操做:

a=nil
new(d);
c->b; 即,将a中对b的引用置为空(你也能够理解为将a中对其余任何内存对象的引用都清空),随即申请d对象,而后在c中增长对b的引用。

因为c已是黑色,因此不会再去扫描他,那么本次内存扫描就不可能找获得b;而d对象因为刚申请出来,尚未被引用,因此这里只对a进行了mark:a:黑色,b:白色;c:黑色;d:白色

这时用户又作了:
b->d; 因为b没法被扫描到,这里显然d也不会被扫描到。 这样的情况会一直持续到这轮反复mark结束(即灰色队列为空)。

stop the world, mark termination。 sweep。 整个GC结束, b,d的内存空间都是白色,因此在sweep时会被清理掉。如何避免这种误清理呢?

写屏障的功能就是在 c->b发生时,对b标记为灰色,入队, 以及在b->d发生时,对d标记为灰色,入队,这样,在整个反复mark阶段结束时,咱们能确保这段时间新发生的对白色对象的内存引用操做都被处理到(变黑),b和d就不会被误清理。写屏障在第一次扫描完,标记入队后,反复标记时开启写屏障, sweep前将写屏障关闭。

简而言之,写屏障的做用大体是: 能够确保不会有对象A直接引用白色对象B(发生时将白色对象置灰)。这里有个小细节,go 1.5中无论对象A是什么颜色,只要他引用了对象B,就将B置灰。