何为GC?
GC:Garbage Collection(垃圾回收)
垃圾指内存中不再使用的内存区域,自动发现与释放这种内存区域的过程就是垃圾回收。
常见的垃圾回收机制:引用计数(python)、标记-清除(go)、分代收集(JAVA)。
引用计数
:对每个对象维护一个引用计数,当引用该对象的对象被摧毁时,引用计数减一,引用计数为零时回收该对象。
标记-清除
:从根变量开始遍历所有引用的对象,引用的对象标记为“被引用”,没有被标记的进行回收。
分代收集
:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不同的回收算法和回收频率。
为什么要有GC?
程序运行过程中会申请大量的内存空间,但内存资源是有限的,而对于一些无用的内存空间如果不及时清理的话会导致内存使用殆尽(内存溢出),导致程序崩溃,因此管理内存是一件重要且繁杂的事情
而垃圾回收可以让内存重复使用,并且减轻开发者对内存管理的负担,减少程序中的内存问题。
Go垃圾回收发展史
版本 | 发布时间 | GC算法 | STW时间 | 重大更新 |
---|---|---|---|---|
V1.1 | 2013/5 | STW | 可能秒级别 | |
V1.3 | 2014/6 | Mark和Sweep分离,Mark、STW、Sweep并发 | 百ms级别 | |
V1.4 | 2014/12 | runtime代码基本都由C和少量汇编改为Go和少量汇编, 包括GC部分, 以此实现了准确式GC,减少了堆大小, 同时对指针的写入引入了write barrier, 为1.5铺垫 | 百ms级别 | |
V1.5 | 2015/8 | 三色标记法,并发Mark,并发Sweep。非分代、非移动、并发的收集器 | 10ms-40ms级别 | 重要更新版本,生产上GC基本不会成为问题 |
V1.6 | 2016/2 | 1.5中一些与并发GC不协调的地方更改。集中式的GC协调协程,改为状态机实现 | 5-20ms | |
V1.7 | 2016/8 | GC时栈收缩改为并发,span中对象分配状态由freelist改为bitmap | 1-3ms左右 | |
V1.8 | 2017/2 | hybird write barrier,消除了STW中的重新扫描栈 | sub ms | Golang GC进入Sub ms时代 |
golang gc 触发条件
GC调用方式 | 所在位置 | 代码 |
---|---|---|
定时调用 | runtime/proc.go:forcegchelper() | gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()}) |
分配内存时调用 | runtime/malloc.go:mallocgc() | gcTrigger{kind: gcTriggerHeap} |
手动调用 | runtime/mgc.go:GC() | gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1}) |
STW(stop the world)
STW的过程中,CPU不执行用户代码,全部用于垃圾回收
Root对象
根对象是mutator不需要通过其他对象就可以直接访问到的对象. 比如全局对象, 栈对象, 寄存器中的数据等. 通过Root对象, 可以追踪到其他存活的对象.
可达性
即通过对Root对象能够直接或者间接访问到.
Mark(标记)
GC 开始,从 root 开始一层层扫描,扫描过程中把能被触达的 object 标记出来,那么堆空间未被标记的 object 就是垃圾了
Sweep(清除)
遍历堆空间所有 object 对未标记的 object 进行清除,清除完成则表示 GC 完成。
golang gc 演变过程
Go 1.1 GC过程
Go 1.3 GC过程
Go 1.5 GC过程
引入三色并发标记法,三色标记法过程如下。
step1:
就是只要是新创建的对象,默认的颜色都是标记为【白色】
step2:
GC回收开始,然后从根节点开始遍历所有对象,把遍历到的对象从【白色】集合放入【灰色】集合
step3:
遍历【灰色】集合,将【灰色】对象引用的对象从【白色】集合放入【灰色】集合,之后将此【灰色】对象放入【黑色】集合
step4:
重复第三步, 直到灰色中无任何对象
step5:
回收所有的白色标记表的对象. 也就是回收垃圾
三色法动态图
1. 正常情况下,写操作就是正常的赋值。
2. GC 开始,开启写屏障等准备工作。开启写屏障等准备工作需要短暂的 STW。
3. Stack scan 阶段,从全局空间和 goroutine 栈空间上收集变量。
4. Mark 阶段,执行上述的三色标记法,直到没有灰色对象。
5. Mark termination 阶段,开启 STW,回头重新扫描 root 区域新变量,对他们进行标记。
6. Sweep 阶段,关闭 STW 和 写屏障,对白色对象进行清除。
写屏障(write barrier)
这里就需要了解一下写屏障的概念。这也是golang1.8如何去除Mark termination的关键。写屏障的目标就是要保障约束: 黑色对象不会指向白色对象,如果被指向了就会出现被清除的白色对象,实际是被引用的对象,造成错误的清理
写屏障的实现有很多模式,在golang1.7之前主要采用的是Dijkstra-style insertion write barrier,其伪码实现如下:
writePointer(slot, ptr):
shade(ptr)
*slot = ptr
其思路就是在进行指针的重定向时,将被指向的指针对象标记为灰色(shade it),这样如果有新的对象被创建或者黑色对象指向白色对象时,目标对象就会标灰,从而满足了黑色对象不会指向白色对象
的约束。
“强-弱” 三色不变式
- 强三色不变式
不存在黑色对象引用到白色对象的指针
- 弱三色不变式
所有被黑色对象引用的白色对象都处于灰色保护状态
为了遵循上述的两个方式,Golang团队初步得到了如下具体的两种屏障方式“插入屏障”, “删除屏障”。
Go 1.8 GC过程
三色标记方式,需要在最后重新扫描一下所有全局变量和 goroutine 栈空间,如果系统的 goroutine 很多,这个阶段耗时也会比较长,甚至会长达 100ms。毕竟 Goroutine 很轻量,大型系统中,上百万的 Goroutine 也是常有的事儿。
Go 在1.8 版本使用混合写屏障(hybrid write barrier)机制,将第一次短暂的 STW取消了,在1.9 版本又大大减少了第二次的STW。
大致如下图所示:
GC开始,默认都是白色
扫描栈区,将可达对象全部标记为黑
Golang中的混合写屏障满足弱三色不变式,结合了删除写屏障和插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间
有疑问加站长微信联系(非本文作者)