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) 回收未标记对象

 

 

posted @ 2022-10-04 16:05  junffzhou  阅读(706)  评论(0编辑  收藏  举报