什么是 GC
垃圾回收(Garbage Collection,GC)是一种自动内存管理机制。
现代高级编程语言管理内存有两种方式:
- 手动:C/C++,Rust等,需要主动申请或者释放内存
- 自动:Java,Golang等,有内存分配器和垃圾收集器自动分配和回收内存
程序中会使用两种内存:
- 堆内存:程序共享的内存,由 GC 进行回收
- 栈内存:线程专用的内存,存储函数的参数值、局部变量等,由操作系统自动分配释放
Go 的垃圾回收
GC 相关术语
- STW:全称 stop the word,GC 期间某个阶段会停止所有的赋值器,中断程序逻辑,以确定引用关系。
- root 对象:根对象是指赋值器不需要通过其他对象就可以直接访问到的对象
GC V1.3
1.3 版本 Go 使用标记-清除作为垃圾回收算法:
- 开启STW,停止程序的运行
- 从根节点遍历,标记找出的所有可达对象
- 清理未标记的对象
- 继续运行程序
重复循环上述 1-4 步,直到 process 程序生命周期结束。标记清除法的最大弊端就是在整个GC期间需要STW,将整个程序暂停。因为如果不进行STW的话,会出现已经被标记的对象A,引用了新的未被标记的对象B,但由于对象A已经标记过了,不会再重新扫描A对B的可达性,从而将B对象当做垃圾回收掉。
GC V1.5
1.5 版本使用标记-清除算法,结合三色标记,插入写屏障:
- 创建白、灰、黑三个集合
- 将所有对象先放入白色集合中
- 遍历所有 root 对象,把遍历到的对象从白色集合放入灰色集合(这里放入灰色集合的都是根节点直接可达的对象)
- 遍历灰色集合,将灰色对象直接可达的对象从白色集合放入灰色集合,自身标记为黑色
- 重复第四步,直至灰色集合中没有任何对象
- 回收白色集合内的所有对象
对于上述的算法来讲,仍然需要依赖STW的。因为如果不暂停程序, 程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改,会影响标记结果的正确性。
在三色标记法过程中对象丢失需要同时满足两个条件:
- 条件一:白色对象被黑色对象引用
- 条件二:灰色对象和白色对象之间的可达关系遭到破坏
Go 团队提出两个解决方案
- 强三色不变式:不允许黑色对象引用白色对象,破坏条件一
- 弱三色不变式:黑色对象可以引用白色对象,但是白色对象的上游必须存在灰色对象,破坏条件二
Go 团队基于上述不变式提出两种实现机制:
- 插入写屏障(满足强三色不变式)
- 规则:当一个对象引用另一个对象时,将另一个对象标记为灰色
- 劣势:无法管理栈操作,需要 STW 重新扫描栈
- 删除写屏障(满足弱三色不变式)
- 规则:在删除引用时,如果被删除引用的对象自身为灰色或者白色,那么会标记为灰色
- 劣势:冗余扫描成本高,回收精度低
GC V1.8
1.8 版本使用标记-清除算法,结合三色标记法和混合写屏障
- GC 开始将栈上的所有可达对象均标记为黑色
- GC 期间,任何在栈上创建的新对象,均为黑色
- 堆上被删除对象标记为灰色
- 堆上被添加的对象标记为灰色
GC 的触发时间
- 主动触发:调用
runtime.GC()
方法 - 被动触发:
- 定时触发,该触发条件由
runtime.forcegcperiod
变量控制,默认为 2 分 钟。当超过两分钟没有产生任何 GC 时,触发 GC - 根据内存分配阈值触发,该触发条件由环境变量 GOGC 控制,默认值为100(100%),当前堆内存占用是上次 GC 结束后占用内存的 2 倍时,触发 GC
- 定时触发,该触发条件由
GC 调优
- 控制内存分配的速度,限制
Goroutine
的数量,提高赋值器mutator
的CPU利用率(降低GC的CPU利用率) - 少量使用
+
连接string
slice
提前分配足够的内存来降低扩容带来的拷贝- 避免
map key
对象过多,导致扫描时间增加 - 变量复用,减少对象分配,例如使用
sync.Pool
来复用需要频繁创建临时对象,使用全局变量 - 增大
GOGC
的值,降低GC的运行频率