golang GC原理
一、堆栈
栈(heap): 由操作系统自动分配释放。一般函数内部执行中声明的变量,函数返回时直接释放,不会引起垃圾回收,对性能无影响
堆(stack): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。堆是在程序运行时申请的动态内存,靠 GC 回收,会影响程序进程
二、GC的触发条件
GC触发条件有以下几种:
1 主动触发,用户代码中调用主动触发 GC
2 定期触发,默认每 2min,golang 的守护协程 sysmon 会强制触发 GC
3 内存分配量超过阈值时触发 GC
三、GC演变
一) GoV1.3之前 标记-清扫法
使用的是标记-清扫(Mark And Sweep)算法
流程:
1) 启动STW: 启动STW(Stop The World),暂停程序
2) Mark标记: 对所有存活的内存单元进行扫描,遍历所有被引用的变量,被引用的对象被标记为“被引用",没有被标记的进行回收,内存单元并不会立刻回收对象,而是将其标记为“不可达”状态。直到到达某个阈值或者到达某个时间间隔后,对其进行垃圾回收
3) Sweep清扫: 垃圾回收
4) 停止STW: 暂停STW,程序继续运行
过程图如下:
缺点:
1) STW程序出现卡顿
2) 标记需要扫描整个heap
3) 清除数据会产生heap碎片
二) GoV1.3 标记-清扫法
对标记-清扫(Mark And Sweep)算法做了优化,减少STW暂停的时间范围
流程:
1) 启动STW
2) Mark标记
3) 停止STW
4) Sweep清扫
过程图如下:
三) GoV1.5 三色标记法
三色标记将对象分为黑色、白色、灰色三种:
黑色:对象在这次GC中已标记, 且这个对象包含的子对象也已标记,表示对象是根对象可达的
白色:未标记对象,gc开始时所有对象为白色,当gc结束时,如果仍为白色,说明对象不可达,在 sweep 阶段会被清除
灰色:被黑色对象引用到的对象,对象在这次GC中已标记, 但这个对象包含的子对象未标记,灰色为标记过程的中间状态,当灰色对象全部被标记完成代表本次标记阶段结束
名词解释:
根对象: 包含了全局变量, 各个goroutine栈上的变量等
标记队列: GC的标记阶段使用"标记队列"来确定所有可从根对象到达的对象都已标记
辅助GC(mutator assist): 为了防止heap增速太快, 在GC执行的过程中如果同时运行的G分配了内存, 那么这个G称为"mutator", "mutator assist"机制被要求辅助GC做一部分的工作,辅助GC做的工作有两种类型: 一种是标记(Mark), 另一种是清扫(Sweep)
1 三色标记流程:
1) 初始时所有对象都为白色
2) gc开始扫描,将所有根对象标记为灰色,放入队列
3) 遍历灰色对象,找到其引用的对象,将引用的对象标记为灰色,将灰色对象标记成黑色
4) 重复以上3步骤,直至没有灰色对象
5) 对所有白色对象进行清除
2 并发问题
在没有用户程序并发修改对象引用关系的情况下,使用三色标记回收没有问题,但如果用户程序在标记阶段更新了对象引用关系,会出现浮动垃圾和对象消失的情况,就可能会导致问题出现,例如: 开始扫描时发现根对象A和B, B引用了C, GC先扫描A, 然后B把C交给A, GC再扫描B, 这时C就不会被扫描到.
浮动垃圾可以在下一次GC回收,对象消失导致程序出现空指针
三色标记中,以下两个问题是不希望发生(实际可能发生):
1) 白色对象直接被黑色对象引用
2) 白色对象没有被其他灰色对象(直接或者间接)引用或者灰色对象与白色对象之间的可达关系遭到破坏
3 屏障机制
并发产生的两个问题,使用如下方式解除:
1) 强三色不变式: 不允许黑色对象引用白色对象
2) 弱三色不变式: 允许黑色对象引用白色对象,但白色对象存在其他灰色对象对它的引用(直接或间接引用)
4 写屏障(Write Barrier)
三色不变式对应golang中GC的写屏障,写屏障只针对指针启用, 而且只在GC的标记阶段启用
写屏障是指编译器在编译期间生成一段代码,该代码可以拦截用户程序的内存读写操作,在用户程序操作之前执行一个 hook 函数,根据 hook 函数的不同,分为 Dijkstra 插入写屏障 和 Yuasa 删除写屏障
基于对栈空间实现写屏障产生的性能损耗和实现复杂度的考虑, golang1.5的写屏障只对堆上的对象使用,不对栈上的对象使用
1) Dijkstra 插入写屏障
堆对象A引用B对象时,B对象被标记为灰色,满足强三色不变式
但因为栈对象没有写屏障,因此,在标记过程中,可能出现 黑色的栈对象 引用到 白色对象 的情况,所以在一轮三色标记完成后 需要开启 STW,重新对 栈上的对象 进行三色标记
插入写屏障的缺点
栈对象没有插入写屏障,在一轮三色标记结束时需要STW来重新扫描栈对象导致程序暂停
2) 删除写屏障
也叫基于快照的写屏障
如果被删除的对象为灰色,则不用处理;如果为白色,那么被标记为灰色,满足弱三色不变式,保护灰色对象到白色对象的路径不会断
删除写屏障的缺点
1) 回收精度低: 被删除的对象在删除的时候要将其置为灰色,而这个对象可能再也不会被其他对象引用,从而导致该对象以及它引用下的其他对象都在本轮GC后被保留下来,要待到下一轮GC才可以被清除
2) 必须在 GC 开启时执行 STW,扫描整个堆栈来记录初始快照,保证所有堆上在用的对象要么为灰色,要么处于灰色保护下,即弱三色不变式
3) 不适用于栈特别大的场景,栈越大,STW 扫描时间越长
5 完整的GC流程:
1) Stack scan: 收集根对象(全局变量和所有goroutine栈上的变量),该阶段会开启写屏障(Write Barrier),将其加入灰色队列
2) Mark: 标记对象,循环处理灰色队列中的对象,直到灰色队列为空,将灰色对象引用的对象标记为灰色,将灰色对象标记成黑色,此时写屏障通过mutator(即辅助GC)记录所有指针的更改
3) Mark Termination: 重新扫描部分全局变量和发生更改的栈变量(栈对象没有插入写屏障,可能会存在新的未扫描的对象),完成标记,该阶段会STW(Stop The World), 导致程序暂停
4) Sweep: 清除回收所有的白色对象
四) GoV1.8 三色标记法+混合写屏障法
go1.5 之后实现了插入写屏障,但是由于栈对象无法使用插入写屏障,导致扫描完之后还需要STW重新扫描栈,混合写屏障就是解决这个问题
混合写屏障(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间,混合写屏障的精度和删除写屏障的一致,比插入写屏障要低
混合写屏障扫描栈虽然没有 STW,但是扫描某个具体的栈时,还是要停止这个 goroutine (针对一个 goroutine 栈来说,是暂停扫的,要么全灰,要么全黑)
1 流程
1) GC扫描堆/栈上的对象并标记为黑色(逐个暂停,逐个扫描,每个栈单独扫描, 无需STW整个程序)
2) GC期间,栈上创建的新对象标记为黑色,堆上创建的新对象标记为灰色(插入写屏障),堆上删除的对象标记为灰色(删除写屏障)
3) 标记结束,重新扫描全局指针,不再rescan栈,并执行其他相关操作
4) 回收未标记对象