对于golang 垃圾回收的了解,我理解更多的就是了解,实际做项目能用到垃圾回收的知识点不多,但有些晦涩难懂的语言,是我们的绊脚石,对于技术怎么能理解就怎么记忆。
1. golang垃圾回收的基础:标记(Mark)、清扫(Sweep)
1)首先把我们的对象简单归个类,有些对象包含基础类型,这类对象回收比较省心,只要该对象没人调用,那就可以回收了。
type aa struct {
A int
B int
}
2)还有对象中包含指针,指针指向另一个对象,这种对象回收 不能只考虑本对象,还要考虑指向的对象,如果父对象都没有回收,却把指针指向的对象删除,那妥妥的内存泄漏。
type aa struct {
A *bb
B int
}
type bb struct {
X int
Y int
}
3)假设应用代码片段如下(不要较真这段程序的对象在栈上分配,示意):
a := aa{}
b1 := &bb{
X: 1,
Y: 2,
}
b2 := &bb{
X: 3,
Y: 4,
}
a.B = b1
a.B = b2
系统有三个对象分别为a、b1、b2
。
4)开始标记根对象,假设跟对象为a为黑色、由于a与b2是父子对象,也标记为黑色。
5)最后发现b1 是白色的,执行清扫流程,然后就回收了。
问题:可以看出标记、清扫逻辑清晰,实现简单,真实GC界的翘楚! 但是,这里有个严重的问题,如果刚扫描完成b2标记为黑色,执行a.B = b1
代码,a又指向了b1,但b1没有被扫描,非常幸运b1被回收了。。。
为了解决这个问题,很简单从源头解决,不让a.B = b1
代码执行就行了,也就是STW
。但对标记整个流程STW
,性能自然是不高的。
2. 三色标记法
1)三色标记法介绍
对上述标记、清扫流程改进,三色标记法。
白色:没有被标记的对象
灰色:被扫描到的对象
黑色:所有子对象都被扫描后,由灰色变成黑色。
还是上面的例子:
- 扫描到a的样子,a变成灰色。
- 扫描到b2的样子,b2 变成灰色
- 扫描完成后的样子,
a、b2
都变成黑色
- 执行清扫流程,将白色的对象回收。
这效果非常棒,成功实现对象回收!!但是… 这和上面的标记、清扫没啥区别啊,STW
问题一个没解决,还换了好几个颜色,崩溃啊!!
2)三色不变性
仔细思考,一个对象被错误回收需要两个必要条件,也就是只要满足下面条件,就会发生内存泄露。
1)强三色不变性:如果出现一个黑色对象指向白色对象。
2)弱三色不变性:白色对象没有一条灰色对象指向他。这句话有点绕,场景:如果一个白色的对象,没有任何一条灰色的对象指向他,则满足第二个条件。
我们上面的案例分析,满足两个条件,黑色指向白色,且没有灰色指向白色,出现内存泄露。
那么只要我们想办法破坏这两个不变性中的一个,就不会出现内存泄露。
3)写屏障
屏障技术网上有很多,这里我们抽取和我们有关的描述,可以在内存操作前执行特定的逻辑。注意:内存在golang程序中分为堆、栈,golang是对堆使用屏障技术,可能是技术实现难度大,毕竟每次栈操作调用其他函数处理、还要保证性能。
插入写屏障
目标:打破强三色不变性,也就是不让对象标记期间,出现黑色指向白色的情况。当有白色对象挂到黑色对象下面时,将白色变成灰色。
还是按照上面的案例,当我们开始垃圾回收的时候,不进行STW,直接开启写屏障,当执行a.B = b1
命令时触发写屏障逻辑,将b1 标记为灰色。最终如下图所示:
有了插入写屏障,打破强三色不变性,对象不会错误回收,也不用STW,完美。但是屏障技术应用在堆上,并没有应用在栈上,假设a对象分配在栈上,那么当
a.B = b1
执行时,不会触发写屏障,那可咋弄?
答:在标记阶段需要STW,扫描栈上指针后,并发标记。这种方式虽然也有STW,但范围上已经小了很多,毕竟堆标记的时候没有STW。这种GC方式在go1.5版本使用。
删除写屏障
目标:我们再思考能否从弱三色不变性下手,打破弱三色不变性。即在标记期间如果有指向白色对象的对象更改引用关系,则直接将该白色对象标记为灰色。上面的例子不够看了,我们再换个例子。
场景:
1)a1、b1对象已经标记完毕
2)a2 已经标记完毕,正在标记b2 的 时候,执行a.B = b2
,如果没有插入写屏障,则b2仍然未被标记。
3)b2被残忍回收了。
删除写屏障:当a2 与 b2 解除依赖关系时,触发删除写屏障,b2标记为灰色。
通过删除写屏障也可以防止内存泄露,但我们还未讨论栈上指针如何处理。
删除写屏障-栈指针处理
1)还是上面的例子,假设a1、a2是栈上对象,上面的例子无法触发删除写屏障,b2内存泄露。
2)a2与b2 解除关系后,b2 与b1进行绑定,如果没有插入写屏障,即使b1是堆对象,也无法触发屏障技术。
上面两种情况分别讨论下:
1)如果能将a2上的指针对象改为a1,说明a1、a2 在同一个goroutine,每个goroutine有独立的栈空间,同一个goroutine发生指针改变,那么后续对栈进行扫描的时候,可以以协程为单位进行STW,还可以并发扫描,时间少很多。
2)这种情况如果能够引入插入写屏障,b2就可以标记为灰色对象。
混合写屏障
go1.8,实现插入写屏障和删除写屏障,STW期间只需要并发检查每个goroutine对象指针变化情况。大大减少STW的时间。
参考:https://cloud.tencent.com/developer/article/2108449