Golang GC
所有的编程语言都有内存的管理办法,这里面分两大类;一类是编程语言提供了手动管理内存的方式,开发者必须自行对内存进行申请和释放,虽然手动管理相对精准,但是编程麻烦且稍有不慎会造成内存泄露和指针乱踩的后果。另一类是由编程语言提供了垃圾收集机制,开发者不需要手动管理内存,这为开发者提供了很大的便利。
但是,垃圾回收的工作机制并不是完美的。
Golang 回收机制
GC 回收的是什么?
在应用程序中会使用到的两种内存,分别是 堆(Heap)和 栈(Stack),GC 负责回收的是 堆内存,而不是回收栈中的内存。
主要原因是栈是一块专用内存,专门为了函数执行而准备的,存储着函数中的局部变量以及调用栈。除此以外,栈中的数据都有一个特点——简单。比如局部变量不能被函数外访问,所以这块内存用完就可以直接释放。正是因为这个特点,栈中的数据可以通过简单的编译器指令自动清理,并不需要通过 GC 来回收。
Go V1.3 之前的标记清除法
标记清除法(mark and sweep) ,低版本的Go 采用的标记清除法很古老。原理也很简单。垃圾回收分为两个阶段:标记阶段和清除阶段。
- 标记阶段:通过根节点(GC Roots)标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。
- 清除阶段:清除所有未被标记的对象。
|
|
但是这样的垃圾回收机制造就了Golang被人诟病的两个槽点:速度慢、效率差
STW(STOP THE WORLD)噩梦 ,在标记清除过程中,会进入一个 “Stop the World” 阶段, 当前运行的所有程序将被暂停。会导致性能下降。
另外标记阶段需要扫描整个 heap,复杂且消耗巨大,最后的清除阶段还会造成heap碎片。
Go V1.5 三色标记法
三色标记法是逻辑上抽象出的三种状态。
白色:对象未被标记,该对象将会在本次GC中被清理
灰色:对象还在标记队列中等待
黑色:对象已被标记,该对象不会在本次GC中被清理
- 初始状态下,所有的对象都是白色。
- 从根节点开始遍历所有对象,把遍历到的对象变成灰色对象(备注:这里变成灰色对象的都是根节点的对象)。
- 遍历灰色对象,将灰色对象引用的对象也变成灰色对象,然后将遍历过的灰色对象变成黑色对象。
循环步骤3,直到灰色对象全部变黑色。
通过写屏障(write-barrier)检测对象有变化,重复以上操作
清除所有的白色对象。
以上图为例,最终对象6由于没有灰色对象引用,最终白色标记表里的对象6被当做 垃圾清除。
写屏障机制
下面主要解释一下步骤5 ,什么是”写屏障机制(Write Barrier)“。
三色标记法在垃圾回收过程中是没有 STW 的。程序在垃圾回收的同时也在正常运行中,为了防止一个被没有被引用的对象(实际上是本来被某对象引用但是取消了引用),在过程中又被已经标记为黑色对象引用,造成误清除,所以必须有一个保护机制即写屏障。
如上图,对象2本引用对象三,但是GC过程中对象2放弃取消引用对象3,转为由对象4引用。那么对象4不会再被扫描,对象3无法变成灰色。
正是为了解决漏标的问题,需要使用写屏障机制来避免,写屏障一定是在内存进行写操作之前执行的。一般屏障机制需要满足以下两个原理来打破上面的破坏。
强三色不变式: 强制性的不允许黑色对象引用白色对象。
弱三色不变式:黑色对象可以引用白色对象,白色对象存在其他灰色对象对他的引用,或者可达它的链路上游存在灰色对象
满足上述两种不变式才能做到GC过程中的误清除。
Go 语言使用两种写屏障技术,分别是 Dikstra
提出的插入写屏障和 Yuasa
提出的删除写屏障。
|
|
插入写屏障
插入写屏障:(满足强三色不变式)在A对象引用B对象时,B对象被标记为灰色(不存在黑色对白色对象引用的情况,因为白色会强制变成灰色)
插入写屏障的缺点:
虽然插入写屏障能解决问题,但是 golang 针对栈上对象的赋值却没有捕捉(没有生成写屏障),原因自然是性能损耗和实现复杂度的考虑。这就开了一个例外的口子,有一些黑色的栈对象指向了白色的对象,而回收器却无法感知到。
golang 的解决方法是:最后再 STW 重新扫描一把栈。这个自然就会导致整个协程的赋值器卡顿。这大约需要 10 ~ 100ms。
删除写屏障
删除写屏障:(满足弱三色不变式)被删除的对象如果自身为灰色或者白色,那么被标记为灰色。(保护灰色对象到白色对象的路径不会断掉)
他的问题是GC过程中如果出现了删除引用,那么所删除的对象及其子对象会全部保留,就是害怕被已被标记的黑色对象再引用。
删除写屏障的缺点:
回收精度低,一个对象即便被删除了最后一个指向它的指针依旧可以活过这一轮,在下一轮GC中被清除。
Go V1.8 混合写屏障
V1.5 通过三色标记 和满足强弱三色不变式的写屏障机制实现了垃圾回收。
但是从上面的过程可以看出,无论是满足强三色的插入写屏障还是满足弱三式的删除写屏障。都有他们的问题。
插入写屏障还需要在结束时STW扫描栈,删除写屏障的回收精度太低。所以在 GoV1.8 版本之后引入了 三色标记 + 混合写屏障机制。
具体操作:
- GC 开始则将栈上的对象全部标记为黑色(之后不再进行二次重复扫描,无需STW)
- GC期间,任何创建在栈上的新对象,均为黑色。
- 被删除对象标记为灰色。
- 被添加对象标记为灰色。
混合写屏障机制满足了变形的强弱三色不变式,(结合了插入、删除写屏障两者的优点)
总结
- Go V1.3 标记清除法:整个过程需要 STW ,效率底下
- Go V1.5 三色标记法:堆空间启用写屏障,栈空间不启用,全部扫描一遍后,需要重新再扫描一次栈(需要STW),效率普通
- Go V1.8 三色标记法+混合写屏障机制:栈空间不启用(栈基本都标记为黑色),堆空间启用,整个过程不需要STW,效率高。