Go 语言中的 GC 问题整理
# GC
GC 清理堆数据,分为手动垃圾回收 和 自动垃圾回收
理论基础:可达性 等价于 存活性
追踪式垃圾回收
三色标记–清扫算法
-> 内存碎片化 ->三色标记-整理算法
(标记完后移动非垃圾数据,使内存更紧凑)复制式回收算法
:将堆内存分为 To 区域和 From 区域,GC 时扫描 From,把有效数据复制到 To 中,最后交换 From 区域和 To 区域- 堆内存里利用率低,一般搭配其他垃圾回收算法使用
分代回收
:基于弱分代假说:大部分对象都在年轻时死亡将经受住特定次数的对象成为老年代对象,其余为新生代对象,新生代对象成为垃圾的概率高于老年代对象
可以降低对老年代对象进行垃圾回收的概率,或者对不同对象采取不同垃圾回收算法,例如
复制时回收算法
引用计数垃圾回收
为每个对象维护一个引用计数,当引用计数为 0 时回收该数据,类似 C++ 中的智能指针
高频率更新计数会带来不小的开销
循环引用问题,C++ weak_ptr 解决(其不控制对象生命周期,不增加引用计数)
用户进程暂停去专注进行垃圾回收,称为 STW(Stop The World)
- 用户进程与 GC 交替执行,称为增量式垃圾回收;但交替执行中用进程可能会修改某些对象,导致 gc 的误判
三色抽象中,黑色对象表示已经处理完,灰色对象还会被继续处理,当全是黑色数据后剩余的白色对象就是垃圾。
对于误判垃圾的情况:黑色对象到白色对象有引用,但没有灰色对象到白色对象的引用,白色对象会被误判为垃圾
强三色不变式:没有从黑色对象直接到白色对象的引用。确保了一旦对象变为黑色,则一定可达,且其引用的所有对象也可达
弱三色不变式:允许黑色对象直接到白色对象的引用,但要保证可以通过灰色对象到达白色对象
通过建立 “读 / 写屏障” 实现 强弱三色不变式
写屏障会在写操作中插入指令,目的是把数据对象的修改通知到垃圾回收器,所以通常会有个记录集
强三色不变式中称为插入写屏障:当一个黑色对象被修改引用到一个白色对象,则将白色对象修改为灰色,或者把黑色对象退为灰色
弱三色不变式中称为删除写屏障:当删除灰色对象到白色对象的引用时,将白色对象修改为灰色
读屏障,非移动式垃圾回收器天然不需要读屏障;类似复制式回收器的移动式垃圾回收期则需要读屏障来保证安全
确保用户程序不会访问到已经存在副本的陈旧对象
- eg.在复制式回收器中,第一段 STW 中,A 从 From 被拷贝到了 To;接着交替执行的用户程序中,B 引用了 A;第二段 STW 中,B 被拷贝到了 To,此时 To 中的 B 还持有着 From 中 A 的陈旧对象。当 From 被清楚后,再通过 B 访问 A 就会出现错误。
多核场景下,分为并行垃圾回收(只有垃圾回收程序执行)和 并发垃圾回收(垃圾回收程序会和用户程序并发执行)
并行垃圾回收会有分工不均引发的负载均衡问题、同步问题、重复处理数据问题(From 到 To 的重复复制)
并发垃圾回收会有锁竞争的问题,比如用户程序和垃圾回收程序会竞争写屏障的记录集。同时因为有多个线程,为了避免同步开启写屏障的延迟,所以一般采用主体并发垃圾回收,在此基础上支持增量式,便是主体并发增量式回收。
# Go 语言的 GC 的简单描述
Go 语言的 GC 采用 标记-清扫算法,支持主体并发增量式回收,使用插入和删除两种写屏障结合的混合写屏障
标记准备阶段(Mark Setup)
STW 暂停程序执行
启动标记工作协程
启动写屏障
找到 roots 放到标记队列中
取消程序暂停
标记阶段(Marking) 和用户进程并发执行
从标记队列中取出对象,标记为黑色
追踪对象,将后面的对象放进标记队列中,标记为灰色
重复 1、2,知道队列为空
扫描过程中,如果用户进程创建或者修改了对象会触发写屏障,将对象放入单独的 marking 队列,标记为灰色
标记终止阶段(Mark Termination)
STW 暂停程序执行
rescan:将 Marking 阶段中触发写屏障产生的队列中对象取出,标记为黑色,并检测是否有指向另一个对象;若有则放入标记队列
关闭写屏障
取消程序暂停
清理阶段(Sweeping)
- 清除所有被标记为白色的对象,内存管理结构中有个 bitmap 区域可以进行标记是否为黑色
GC 触发时机
主动触发:调用
runtime.GC()
方法被动触发:
定时触发:由
runtime.forcegcperiod
变量控制,默认为 2 分钟。当超过 2 分钟没有 gc,则触发 gc根据内存分配阈值触发:由环境变量
GOGC
控制,默认为 100。如果当前堆内存是上次 GC 结束后占用内存的 2 倍时,则触发 gc