golang 垃圾回收(GC)分析
之前写过一篇GC的总结,但是太过简陋。现在重拾八股,又有了很多疑问,重新总结一下GC的原理
golang采用的GC算法是三色标记法+混合写屏障
三色标记法其实是标记清除算法的一种,主要分为两个过程:标记和清除
三色标记法
三色标记法标记过程主要通过三个阶段的标记来确定哪些对象需要清除:
- 第一步,程序创建之初,所有的对象都被标记为白色,放到白色集合中
- 第二步,在GC开始的时候,会从根节点开始遍历,把遍历的到的白色对方放到灰色集合中
- 第三步,遍历灰色对象结合,将拿出的灰色对象引用到的对象放入灰色集合集合,将这个被拿出的灰色对象放入到黑色集合中
- 然后重复第三步,直到灰色集合中再没有对象
- 最后就是清除阶段,回收掉所有的白色对象,就是垃圾回收
这个算法其实就是广搜的思想,单向图从根节点遍历所有能遍历到的节点,遍历不到就是白色对象,就是GC需要回收的垃圾
如果只是单独的三色标记清除是有问题的,就是如果GC期间产生了对象丢失问题,比如黑色对象引用了一个白色的对象,同时这个白色对象和它上游的灰色对象引用关系遭到破坏,那么这个白色对象和它的下游对象就会在清除阶段被清除。
解决这种方法最简单的办法是stw(stop the world)
STW
就是让程序暂停,这样GC期间对象就不会更改,但是这样会让程序发生卡顿,这对所有的用户程序都有很大影响
屏障技术
当然还有其他方法,就是从产生这种问题的源头入手:
- 黑色对象引用了白色对象
- 同时白色对象和它上游的灰色对象引用关系被破坏
由此产生了两种思想,就是强-弱三色不变式:
(1) 强三色不变式:强制性的不允许黑色对象引用到白色对象
(2) 弱三色不变式:不再强制不允许引用,但是黑色对象引用的白色对象的上游必须有灰色对象保护,这样就可以保护白色对象
基于对上述两种思想,golang算法演进了两种屏障方式"插入屏障"、"删除屏障"
- 插入屏障(强三色):
- 在A对象引用B对象的时候,将B对象标记城灰色
- 但是因为要保障栈的高效率,写屏障对栈不生效。因此栈不会标记,这样栈上新加的对象依旧是白色。需要在GC之后再启动stw对栈进行扫描
- 删除屏障(弱三色):
- 被删除的对象,如果本身是灰色或者白色,那么被标记成灰色
- 这种回收方式精度低,一个对象即使被删了依旧可以活过这一轮GC
混合写屏障
在1.8版本启用了混合写屏障,来减少stw的时间
-
规则如下:
- GC开始时,优先扫描栈,将栈上所有可以扫描到的对象标记成黑色(之后不需要进行二次重复扫描,无需stw)。之后会开始三色标记
- GC期间,栈上创建的新对象,均标记成黑色
- 堆上被删除的对象标记成灰色
- 堆上被添加的对象标记成灰色
-
这个其实是变形的弱三色不变式,而且屏障技术并不引用在栈上,要保证栈的运行效率
-
几个案例分析
- 对象被一个堆对象删除引用,成为栈对象的下游
- 对象被一个堆对象删除引用,成为另一个栈对象的下游
- 对象被一个栈对象删除引用,成为另一个栈对象的下游
- 对象被一个栈对象删除引用,成为另一个堆对象的下游
-
延伸一下:
- GC期间,在扫描栈之后,栈上黑色对象是无法引用到白色对象的,比如对象9引用对象5
- 为什么呢,因为这些对象是不可到达的,如果可到达就不会被标记成白色了,就比如局部变量的在被调用结束之后,生命周期立刻结束。
值得注意的是,混合写屏障同样在栈上不生效,只在堆空间启动,而且标记阶段几乎不用stw。所以三色标记+混合写屏障,效率极高
v1.8版本GC过程
- STW,开启混合写屏障,扫描栈对象
- 三色标记启动
- STW,关闭混合写屏障
- 在后台进行GC(利用并发优势)
1.8之后的GC并不是不需要GC,而是再三色标记阶段不需要GC,这样极大的减少了STW时间,效率极高。只会在开启混合写屏障和混合读屏障的时候开启stw
参考: 刘丹冰老师的GC分析
转载自:https://juejin.cn/post/7346102724498538546