垃圾回收,Garbage Collection,GC
GC几乎成为现代,偏向于业务型高级编程语言的标配,它解放了内存释放的种负担,可以让程序开发者把精力放在业务的开发上,而非内存的分配与管理上。这极大的提升了程序的开发效率,也保证了程序的一部分内存安全性,但随之而来的问题就是,GC的算法直接影响着垃圾回收的效率。早期的Go语言,其垃圾回收过程对于用户来讲甚至可以感知到(肉眼可见的卡顿)。Go语言在版本不断迭代的过程中,对垃圾回收算法进行了优化。
经典的垃圾回收算法
标记-清除(Mark-Sweep)
标记清除算法是一种间接的垃圾回收算法,它不直接对垃圾进行回收,其执行过程中分为两个阶段,首先是扫描并标记仍被引用(活着)的对象,而后清除没有标记的对象(即垃圾)。该算法会延着引用路径进行深度扫描。该算法的弊端是容易产生内存碎片。
标记-压缩(Mark-Compact)
标记压缩算法先执行标记操作,将内存区域中被引用(活着)的对象做标记,然后将被打标记的对象移动到连续的内存空间,该内存空间后边的区域也就是垃圾所在的区域,也就是可以被再次分配使用的内存区域。该算法的弊端是,容易破坏CPU缓存的局部性原理,同时标记对象需要额外的空间,以及需更新对象的多级引用情况。
半空间复制(Semispace Copy)
该算法以空间换时间,只使用一块分配好的内存空间的一半保存对象,另一半暂时保持空闲,当GC时直接将扫描到的被引用的对象直接复制到另一半空闲的内存区域,整轮GC结束后,存活的对象只保留在内存空间的一侧,该算法实现起来容易,但缺点就是浪费内存,容易破坏CPU缓存的局部性原理等。
引用计数
该算法让每个对象都持有一个计数器,当其他对象引用对象时,计数器则自增,若被取消引用,计数器则自减。若引用计数为0,则证明该对象为垃圾,需要将其回收。该算法简单,直接。但缺点是自增自减需要原子操作,在一些临时对象上也需要更新其计数器,有时需要栈上与寄存器中的对象同时更新计数器。同时该算法,对自我引用的对象很难处理。
分代垃圾回收
该算法算得上是开发者根据长期的经验总结发现,“大部分的对象在生成后马上就变成了垃圾,很少有对象能活得很久”。根据对象的生命周期不同,将其按"年龄"分代,对每一轮GC结束后,仍存货的对象进行"年龄"的重定义,使之进入相应的代(辈分区域)。不同代的GC算法也不同,可以综合以上的算法实现分代GC,分代GC可以分多代。其在并发情况下做GC十分复杂。
STW
STW 是Stop The World(停止世界——做GC) 与 Start The World(启动世界——恢复程序正常运行)的缩写,它一般指Stop The World 到 Start The World之间的时间间隔,此时间间隔越小,用户代码运行的就可以越流畅。Go STW的作用和其他带GC的语言类似,都是防止创建新对象,或销毁对象,以及对象间产生新的依赖关系,对上一轮清理工作的影响。
Go语言GC算法
Go语言采用的是标记-清除(Mark-Sweep)算法做垃圾回收。
Go 1.0
串行标记清除 + STW
Go 1.3
在Go 1.3版本中垃圾回收算法采用 并发标记清除+STW,其算法流程如下:
- Stop The World
- 做存活对象的标记工作
- 做未被标记的垃圾的清理工作
- Start The World
开发者发现该算法STW的时间过长,容易造成程序卡顿,特别是对有时效性限制的程序更不友好,于是对该算法的执行顺序做了调整。
- Stop The World
- 做存活对象的标记工作
- Start The World
- 做未被标记的垃圾的清理工作
将垃圾清理工作拿到STW外边与用户代码并行执行,Go的效率有所提升,但其GC的工作仍会拖累Go的执行效率。
Go 1.5
在Go 1.5版本中垃圾回收算法采用的是 标记清除算法中的三色标记法 + STW。
并发三色标记法
该算法将对象分为黑、灰、白三种颜色,新创建的对象默认为白色。算法中会涉及到几个概念:
1.Root Set: 保存所有对象引用关系的根集合,包括全局变量,栈上变量,寄存器的值
2.白色标记表: 可能不存在被引用关系对象的列表,最开始所有对象都在该列表
3.灰色标记表: 被黑色对象引用的对象的列表
4.黑色标记表: 存在被引用关系对象的列表
算法流程:
1.遍历所有Root Set中的对象,将Root Set中的对象从白色标记表移动到灰色标记表,同时标记其为灰色。
2.遍历灰色标记表,将灰色标记表中引用的对象从白色标记表移动到灰色标记表,同时将其标记为灰色,然后将之前的对象移动到黑色标记表,标记成黑色。
3.重复步骤2,直到灰色标记表为空
4.此时回收白色标记表中的对象,其为垃圾,没有被引用。
若在GC的过程中,白色标记表中的某个对象被灰色标记表中的某个对象引用着,简称引用1,此时发生并行操作,黑色标记表中的某个对象突然引用上述白色标记表中的那个对象,简称引用2,此时引用1突然断开。那么引用2没有断开,但是引用2引用的白色对象因错过了标记,所以仍会被回收掉。这便出现了对象丢失的问题。解决该问题的办法是强弱三色不变性。
强三色不变性
不允许被标记为黑色的对象引用被标记成白色的对象。
弱三色不变性
在被标记为黑色的对象引用被标记为白色的对象的时候,被标记为白色的对象必须仍被链路上的某个被标记为灰色的对象直接引用或间接引用。
引入这两种方案,便解决了并行操作中GC时可能存在的对象丢失问题。
强弱三色不变性的具体实现,屏障机制
屏障与Hook、回调函数思想类似。
插入屏障 对象被引用时触发
// Dijkstra 插入屏障
func DijkstraWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer){
shade(ptr) //标记对象为灰色
*slot = ptr // 当前下游对象slot = 新下游对象ptr
}
具体操作:在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A的下游,B必须被标记为灰色)
满足:强三色不变性。(不存在黑色对象引用白色对象的情况,白色对象会被强制变成灰色)
插入屏障不在栈上使用,为了保证栈的速度。
在执行GC的时候,Root Set根节点集合被划分为栈区域和堆区域。对栈上对象不用插入屏障,对堆上对象使用插入屏障。并行的执行三色标记回收算法。
在执行完上述操作后,Stop The World栈空间,然后对栈上对象重新执行三色标记算法 而后Start The World栈空间。
STW 仍会有少部分的暂停操作,所以对程序整体性能仍有一部分影响。
删除屏障 对象被删除时触发
// Yuasa删除屏障
func YuasaWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer){
shade(ptr)
*slot = ptr
}
具体操作:被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
满足:弱三色不变性。(保护灰色对象到白色对象的路径不会断)
执行GC仍然会涉及到STW。
并延迟一轮删除被标记的对象,即让下轮GC去删除断开引用的对象,回收精度低。
Go 1.8
并发三色标记法+混合写屏障机制
混合写屏障
// 混合写屏障
func HybridWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer){
shade(*slot) //二者均标记为灰色
shade(ptr)
*slot = ptr //新的下游对象为灰色
}
具体操作:
1.GC将栈上的对象全部扫描并标记成黑色(之后不再进行第二次重复扫描,无需STW)
2.GC期间,任何栈上创建的新对象,均为黑色。
3.被删除对象被标记为灰色
4.被添加的对象被标记为灰色
满足:变形的弱三色不变性。(结合插入、删除写屏障的优点)
GC开始时优先扫描栈,将栈上可达对象全部标记为黑色。
堆上对象启用混合写屏障。
Go 1.10
并发三色标记法+批量写屏障机制
将主色的指针统一放入缓存,缓存满时才对缓存中的ptr进行着色
触发GC时机
主动触发: 调用runtime.GC()
被动触发:
1.使用系统监控,超过两分钟没有产生GC,强制触发GC
2.使用步调算法,控制内存增长比例
Reference
https://www.yuque.com/aceld/golang/zhzanb
《Go 语言底层原理剖析》
《Go 程序员面试笔试宝典》
《垃圾回收的算法与实现》