概述
回收线程(collector) 和 其他线程(mutator)可并行执行,允许多个回收线程并行。
- 类型精确 (type accurate)
- 非代龄 (non-generational)
- 非收缩 (non-compacting)
- 写屏障 (write barrier)
- 并发标记 (mark) 和 清理 (sweep)
回收周期
回收周期包含以下几个步骤:
1. 清理结束
- STOP-THE-WORLD, 让所有P到达安全点。
- 清理所有未及时清理的内存块。(清理未结束前执行
runtime.GC)
2. 开始标记
-
状态 GCoff -> GCmark。
- 启动写屏障。
- 启动辅助(mutator assissts)。
- 根标记任务排队。
- 所有P写屏障启用前,不扫描任何对象。
-
START-THE-WORLD。
- 调度器(schedule)启动标记工人(mark workers) 和分配辅助(assists)。
- 写屏障遮蔽指针写入(shades both the overwritten pointer and the new pointer value)。
- 新分配对象(malloc)直接标记为黑色。
-
执行根标记任务。
- 扫描所有栈(stack)。(会导致goroutine停止,扫描完成后恢复)
- 遮蔽所有全局变量(global)。
- 遮蔽堆外运行时(off-heap runtime)数据结构中的堆指针。
-
清空灰色队列。(drains the work queue of grey objects)
- 扫描其中灰色对象(grey object),将其标记为黑色。
- 对其包含的指针进行着色,将引用目标放入灰色队列。
-
终止算法(distributed termination algorithm)检测何时不再有根标注任务和灰色对象。
- 标记结束
3. 标记结束
- STOP-THE-WORLD。
- 状态
GCmarktermination,禁用标记工人和分配辅助。 - 处理内部任务。
4. 执行清理
-
状态
GCoff,设置清理状态,禁用写屏障 -
START-THE-WORLD
- 从此刻起,新分配对象为白色,必要时会在分配前清理目标对象。
-
后台执行并发清理(oncurrent sweeping),并响应分配操作。
5. 重新执行
- 分配超过阈值,重新回到第1步
并发清理
清理与用户逻辑并发执行,专门的goroutine在后台挨个清理。标记结束后,所有span都被标记为需要清理。
为避免向OS申请过多内存,首先尝试清理现有span以获取可复用空间。确保不会在未清理的span上执行操作,避免破坏标记位图。回收期间,mcache所持有的span全部收回mcentral。重新获取时,会执行清理操作。而当下一回收周期启动时,也会先完成未清理任务。
回收速率
环境变量GOGC控制了回收和分配间的线性比例。如果GOGC=100,使用了4MB,那么到达8MB时,垃圾回收将被再次启动。
控制器
控制器(gcController)用于GC调控,决定何时触发,有多少工作量,需要多少投入和辅助。基于每个回收周期的堆增长和CPU利用率等数据,以反馈算法(feedback control algorithm)进行调整。该算法将辅助标记和后台标记的CPU利用率优化为GOMAXPROCS的25%。
垃圾回收有个大问题需要解决,那就是什么时候启动?过早或频繁启动,除浪费cpu资源外,还会加大用户逻辑停顿时间。过晚又会导致堆膨胀,浪费更多内存。如何在两者间平衡,是个巨大的挑战。
并发回收阶段,对象分配速度可能远快于回收标记。这会引发一系列恶果,比如堆恶性扩张,或导致单个回收周期无法结束,以至于垃圾回收机制瘫痪。此时,暂停用户逻辑,让该线程临时参与辅助回收就十分必要。此举非但能抑制用户逻辑在短时间内大批量分配内存,还可提升回收效率。平衡分配和回收,更充分复用内存。
写屏障
直观上看,写屏障是编译器在用户逻辑内插入的额外指令。
因三色标记和用户逻辑并发执行,那么已检查的黑色对象就可能被修改。假设已扫描黑色对象内部指针“突然”指向一个尚未扫描的白色对象。按三色标记流程,按三色标记流程,黑色不会再次扫描,如此就导致该白色对象最终被回收,从而引发逻辑错误。
A(黑)引用B(灰); B引用C(白)。\
然后,A引用C,B不再引用C。\
如没有写屏障,那么A不会再次扫描,C保持白色被回收。
写屏障启用后,对指针的修改会跳转到写屏障指令,以便对其重新标记、扫描。如此,其引用的白色对象会存活下来。写屏障解决了垃圾回收与用户逻辑并发执行的冲突,有助于减少重新扫描次数,简化和消除了某些复杂的机制。
写屏障仅在垃圾回收时启动,通过特定开关进行判断。
正常情况下,用户逻辑不会跳转到这些额外的指令,性能不受影响。
三色标记
扫描内存时,使用三种颜色标记对象状态。
-
起初,所有对象默认为白色
-
扫描,可达对象如包括指针,标记为灰色后放入待处理队列,否则直接黑色。
-
依次从队列提取灰色对象,扫描其指针字段。
- 该灰色对象标记为黑色,表示存活。
- 该字段所引用对象,标记为灰色后放回队列。
-
扫描和队列结束,仅剩黑白两色
- 黑色为存活对象。
- 白色表示待回收对象。
通过队列实现递归扫描,找出存活(黑色)对象,其余被回收。这就是三色标记原理,大体与扫描流程相对应。至于用户逻辑在扫描阶段新分配的用户对象,则直接标记为黑色。
标记操作对于:\
a := make([]*int, 1e9)\
b := make([]int, 1e9)\
性能差别巨大。因为b无需深入内部扫描,故而快的多。
所以,对于超大内存使用要慎重。
比如说,分配一大块[]byte或mmap,持有阻止回收。然后,在内部使用uintptr二次分配。
本文正在参加技术专题18期-聊聊Go语言框架