简介
Go的GC主要针对堆内存,而不是栈内存。因为:
- 栈内存自动分配和释放,遵循先进后出原则。
- 函数调用信息和局部变量会根据栈指针变化被自动清理。
Go主要通过三色标记法来标记和扫描所有堆对象,然后通过混合写屏障来保证对象不丢失,并减少STW(stop the world,即暂停程序)时间。
三色标记法
工作流程
GC维护三个集合:白色标记集合、黑色标记集合、灰色标记集合。首先把所有对象标记为白色,全部加入白色标记集合,然后执行下面的操作进行标记:
- 扫描根对象,得到根对象引用的对象,标记为灰色,全部加入灰色集合。遍历后的根对象加入黑色集合。
- 遍历灰色标记集合,将可达对象标记为灰色,遍历后的灰色对象标记为黑色,分别加入灰色标记集合和黑色标记集合。
- 重复上一步,只到灰色标记集合没有任何对象。
- 收集所有白色对象,即垃圾。
这里的根对象主要是指:全局变量、栈上的变量、寄存器中的变量等。
需要STW吗?
三色标记法如果不使用STW,会出现对象丢失的问题。例如:
- 一个白色对象没有任何被引用,但是在标记时,被黑色对象引用了。由于不会重复扫描这个黑色节点的引用,所以这个白色对象会被判断为垃圾。
- 如果一个新的对象被创建,标记为白色,最终会被当作垃圾。
而三色标记法如果在执行时一直使用STW,会时程序出现明显的卡顿。因此有两个解决方法
- 强三色不变式:强制黑色对象不能引用白色对象。
- 弱三色不变式:允许黑色对象引用白色对象,但是需要这个白色对象上游有灰色对象。
这两个方法需要通过屏障机制实现。
混合写屏障
混合写屏障由插入屏障和删除屏障组成。
插入屏障
当黑色对象引用白色对象时,将白色对象标记为灰色,为了性能Go仅在堆上插入写屏障,栈上不启用。缺点是在垃圾回收完成前,需要重新STW,并重新扫描整个栈空间,保证没有遗漏的引用关系。传统的Dijkstra插入屏障有一个问题:栈上不应用写屏障(为了性能考虑,不然每次栈操作都要应用写屏障),这可能导致以下情况:
- 栈上对象引用了一个白色对象
- 这个引用关系被移除
- 没有写屏障保护,这个白色对象可能被错误回收
所以在使用插入屏障时,必须在GC结束前重新STW并扫描整个栈,确保没有遗漏的引用。
删除屏障
某个引用删除时,如果被引用对象是白色或者灰色,则将其标记为灰色。满足弱三色不变式,将主动可能丢失的对象提升为灰色。缺点是回收精度低,某些实际不可达对象,要下一轮GC才能回收。
混合写屏障
GC开始:栈上所有元素都标记为黑色 GC期间:
- 任何新创建对象都标记为黑色。(栈上之前的对象和新对象都是黑色,不会被错误清除)
- 被删除引用的对象标记为灰色。
- 被添加引用的对象标记为灰色。
垃圾回收的时机
Go的GC触发时机由多种因素决定。常见的有
基于内存分配阈值触发
环境变量GOGC控制GC触发频率,默认值为100。默认GOGC=100意味着当内存使用量达到上次GC后存活内存的2倍时触发新的GC。
基于时间触发
如果超过一定时间,约2分钟没有执行GC,会触发强制GC,保证即使在内存使用较少的情况下,GC也会定期执行。
手动触发
调用runtime.GC()可以手动触发。
import "runtime"
func f(){
runtime.GC()
}
系统内存压力触发
通过GOMEMLIMIT环境变量可以设置Go程序最大内存使用量。当系统资源紧张,可能触发提前GC。
标记辅助触发
分配内存速度太快,GC跟不上分配速度时触发的GC。