前言
前情回顾:
终于来到GC这块了。Go的GC是在运行程序时才工作的,并且是异步的,类似一个后台进程,不会打扰到其它协程的工作。那GC是怎么实现的呢?
可达性分析
Go的GC是采用标记清除的方式,先把所有有用的对象标记了,然后再回收未标记的对象。
那从哪里开始找呢?
从Root Set变量开始找,Root Set变量有以下特征:
- 被栈上的指针引用
- 被全局变量指针引用
- 被寄存器指针引用
遍历方式是什么?
内存分布类似图或树,举例如下:
对于这类结构,可以采用深度dfs遍历或广度bfs遍历。Go的GC采用的是广度优先遍历方式。
首先从根节点出发,将引用的到节点标记上:
继续下一轮标记:
标记完成后,发现对象G和H未被标记,便将其内存回收。
以上方法称为可达性分析法,其步骤为:
- 启动前要暂停其它协程
- 通过可达性分析,找到无用的堆内存
- 释放堆内存
- 恢复所有协程
这种串行GC方法实现起来较为容易,但缺点也很明显,那就是需要暂停其它协程,对性能影响大。
三色标记法
要解决GC时的性能问题,就得使用并发GC了,因此Go提出了三色标记法。
三色标记法分别是黑色,灰色和白色,不同标记颜色意义不同:
- 黑色,有用,已经分析扫描完了
- 灰色,有用,暂未分析扫描
- 白色,暂时无用
运行时也是采用可达性分析的思路,先标记再清除。其运行过程如下。
标记阶段
首先初始状态如下,用三个队列分别放置黑灰白对象:
从Root Set出发,发现用到了对象A和E,将自身标记为灰色,加入灰色队列。为什么是灰色呢?因为对象A和E是有用的,但还没有对其子对象进行分析扫描。
下一步从对象A和E出发,发现对象B,C,D,F,将这些对象都标记为灰色,加入灰色队列。而对象A和E扫描完了,标记为黑色,加入黑色队列。
最后扫描分析对象B,C,D,F完成后,都标记为黑色,加入黑色队列。同时广度搜索完毕。
清除阶段
将对象G,H堆内存回收。
再次标记时,所有对象置为白色。
以上即GC的运行过程,但在并发场景下,可能存在一些问题。
删除屏障
并发场景下,可能存在误清理情况。
首先,当一个白色对象更改为被黑色对象引用,就会被误清理。演示过程如下:
某时刻状态:
由于并发运行,其它业务可能取消对象B引用对象D,而更改为被对象E引用了。
最终GC结果如下:
因为对象E已经标记为黑色了,不会再对子对象扫描,那对象D就一直标记为白色了,最终会被GC误清理了。
怎么解决呢?该问题关键在于对象D始终为白色,无法被扫描到。
因此Go采用了删除屏障的方法,当一个对象被取消指针引用时,自身会被标记为灰色。也就是说,对象D被对象B取消引用时,自身会被标记为灰色。
之后对象E引用对象D,扫描到对象D时,D就会标记为黑色了,从而避免误清理情况出现。
插入屏障
还有个问题,如果插入一个新对象H,该对象被黑色标记的对象E引用了怎么办呢?如下图:
GC也不会扫描到对象H,使对象H始终为白色标记,从而误删除。GC结果如下图:
由于对象H为白色标记,导致误删除。
怎么解决?该问题关键也是在于对象H始终为白色,无法被扫描到。
因此Go采用了插入屏障的方法,当一个黑色标记的对象引用一个新对象时,新对象会被立即标记为灰色。如下图:
当黑色对象E指向新对象H时,H会被置灰,便不会被误清理了。
已经被引用的对象取消了指针引用,会触发删除屏障,将该对象置灰。
插入的新对象被黑色标记对象引用时,会触发插入屏障,将新对象置灰。
GC触发的时机
那GC在什么时候触发呢?有三个时机:
- 系统定时触发
- 用户主动调用
- 申请新内存时触发
系统定时触发调用了sysmon定时检查,如果两分钟内没有GC,则触发。
用户通过调用runtime.GC()来主动触发GC。
申请新内存时调用了mallocgc方法,也会触发GC。
结语
以前就听过三色标记法,今天终于入门了。
至此,堆内存结构,分配和回收堆内存都已完毕!