Go 垃圾回收原理
目录
内存管理
标记清除法——Go1.3
三色标记法——Go1.5
三色+混合写屏障——Go1.8
GC 触发的时机
01 内存管理
1)程序里面的垃圾是怎么产生的
程序在内存上被分为栈区、堆区、数据区、全局数据区、代码段五个部分。
对于 C++ 等底层的编程语言,栈上的内存空间由编译器统一管理,而堆上的内存空间需要程序员来手动管理进行分配和回收。
在 Go 语言中,栈上的内存空间也是由编译器来统一管理,而堆上的内存空间由编译器和垃圾收集器共同管理进行分配和回收,这给我们程序员带来了极大的便利性。
垃圾其实就是程序向堆栈申请的内存空间,随着程序运行已经不再使用了。而这些无用的堆栈对象占用了机器大量的内存空间,如果不把它们都回收,那么机器的内存就会越来越小,最终无法提供正常的服务,这就是内存泄漏。
2)内存管理
而内存的管理,就是管理可用内存空间的方式。想要对内存空间进行合理化使用,一些好的垃圾回收(Garbage Collection,以下统一简称 GC)算法是必不可少的。接下来,我们看一下 Go 语言的 GC 是怎么实现的。
02 标记清除法
Go 1.3 GC 用的是标记清除法,每次 GC 时线程暂停会延迟几百 ms。
原始的标记清除分两步:
标记,先 STW(Stop The World),暂停整个程序的全部运行线程,将被引用的对象全部打上标记;
清除未打标的对象,即回收内存资源,然后恢复运行线程。
这样做有个很大的问题就是,要通过 STW 来保证 GC 期间标记对象的状态不能发生变化,整个程序都要暂停几百 ms,在外部看来程序就会卡顿。
03 三色标记法
Go 1.5 GC 用的是三色标记法,这种算法实现了并发清除垃圾,STW 延迟缩短到了10ms 以下。
1)对象颜色分类
三色标记法将程序中的对象分为以下三类:
白色对象,表示暂无对象引用的潜在垃圾,其内存可能会被垃圾回收器回收;
灰色对象,表示活跃对象,白色到黑色的中间状态,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;
黑色对象,也表示活跃对象,包括不存在引用外部指针的对象和从 root 区域出发扫描到的对象。
root 区域主要是程序运行到当前时刻的栈和全局数据区域。
2)三色标记流程
标记过程如下:
初始状态所有对象为白色;
从 root 根出发扫描所有对象,将它们引用的对象标记为灰色;
分析灰色对象是否引用了其它对象:
如果没有,则将该灰色对象标记为黑色;
如果有引用,则将该灰色变为黑色的同时,将它引用的对象变为灰色;
重复步骤3,直到所有灰色对象集合为空时,标记阶段结束。此时,对所有白色对象进行回收。
结合如下动图理解:
可以看出:A、F 为 root 根直接引用的对象,E、G、H 对象为垃圾,需要回收。
3)错误回收与三色不变式
由于用户程序可能在执行标记的过程中修改对象的指针,所以三色标记法可能会出现错误回收的情况,例如:
三色标记执行时产生如下情况:
根集合上的节点 A 已经变成了黑色,它指向的对象 B 变为了灰色;
A 对象新增指针,指向了对象 D;
持续扫描所有灰色对象,将其指向对象变为灰色,自身变为黑色,直到灰色集合里没有对象。
我们发现,在这个过程中,D 对象一直是白色的,所以扫描结束后,它会被垃圾收集器错误地回收,影响了内存的安全性。
因此,想要在标记过程中保证安全性,我们需要达成以下两种三色不变式(Tri-color invariant)中的任意一种:
强三色不变式:黑色对象不能指向白色对象,只能指向灰色或黑色对象;
弱三色不变式:黑色对象指向的白色对象,必须包含一条从灰色对象经由多个白色对象的可达路径。
如图所示:
当三色标记算法遵循以上两个不变式中的一种,就能保证垃圾回收时内存的安全性。而为了实现这两种不变式,引入内存屏障技术。
4)屏障技术
屏障技术是在用户程序读取、创建以及更新对象指针时执行的一段代码,简单来说,内存屏障是一种能够保证内存操作顺序的技术。根据操作类型的不同,屏障技术分为读屏障和写屏障两种。
由于读屏障需要在读操作时加入代码片段,对用户程序的影响较大。因此,Go 语言采用写屏障来保证三色不变式,写屏障技术包括 Dijkstra 在 1978 年提出插入写屏障和 Yuasa 在 1990 年提出删除写屏障两种。
5)插入写屏障
插入写屏障的原理是:当有黑色对象 A 指向新对象 D 时,如果被指向对象 D 为白色,则将 D 对象设置为灰色,它实现了强三色不变式。
如上图的标记过程:
垃圾收集器将根集合上的 A 对象标记为黑色,并将 A 对象指向的 B 对象标记成灰色;
用户程序修改 A 对象的指针,将原本指向 B 对象的指针指向 C 对象,这时,触发插入写屏障将 C 对象标记为灰色;
垃圾收集器依次遍历其它灰色对象,标记为黑色。
插入写屏障将被添加引用的白色对象都标记为了灰色,这种方法实现简单,但也有明显的缺点:
缺点一:未存活的对象可能需要两次回收,假设上述第 2 步到第 3 步之间,A 对象的指针又从指向 C 改为指向 B,那 C 对象就是垃圾,应该回收。但是此时灰色的对象 C 会被垃圾收集器认为是存活的,这些被错误标记的对象只有在下一个循环才会被回收;
缺点二:写屏障只会对堆上的内存对象启动写屏障(插入和删除写屏障共有的)。而栈上的对象需要保证内存安全时,必须在标记阶段重新对栈上的对象进行 STW 扫描。重新扫描时需要暂停程序,影响整体性能。
6)删除写屏障
删除写屏障的原理是:对象 A 被引用时,如果引用它的对象被删除了,那么白色的 A 对象将被标记为灰色,它实现了弱三色不变式。
如上图的标记过程:
垃圾收集器将根集合上的 A 对象标记为黑色,并将 A 对象指向的 B 对象标记成灰色;
用户程序将 A 指向 B 对象的指针,修改为指向 C 对象。这时,触发删除写屏障,但因为 B 对象不为白色,所以不做改变;
用户程序将 B 指向 C 对象的指针删除,触发删除写屏障,此时,白色的 C 对象被标记为灰色;
垃圾收集器依次遍历程序中的灰色对象,将它们标记为黑色。
删除写屏障将被删除引用的白色对象都标记为了灰色,但是它也有缺点和局限性:
缺点一:回收精度低,当对象 B 已经被删除时,它仍然可以活过这一轮在下一个循环才被回收;
缺点二:同插入写屏障,重新 STW 扫描栈对象。
04 三色+混合写屏障
基于插入写屏障和删除写屏障在结束时需要 STW 来重新扫描栈上的内存对象,考虑其带来的性能影响,Go 1.8 GC 组合了插入写屏障和删除写屏障构成了混合写屏障以及原本的三色标记法,将垃圾收集的时间缩短到 0.5ms 以下,整个流程分为以下四步:
GC 开始时,将栈上所有的可达对象全部标记为黑色(不需要二次扫描,无需 STW);
GC 期间,任何栈上创建的新对象都标记为黑色;
将被删除引用的对象标记为灰色;
将被添加引用的对象标记为灰色。
05 GC 触发的时机
触发 GC 有两个条件:
Go 语言运行时的默认配置会在堆内存达到上一次垃圾收集的 2 倍时,触发新一轮的垃圾回收,这个行为可以通过 GOGC 变量调整。它的默认值为 100,即增长 100% 的堆内存才会触发 GC;
如果一定时间内没有通过方式 1 触发,也会触发新的循环,这个定时时长由 runtime.forcegcperiod 变量控制,默认为 2 分钟。
06 参考文献
《Go语言设计与实现》—— Draven
搞懂 Go 垃圾回收,https://juejin.cn/post/6844903917650722829#heading-0
图解垃圾回收机制,https://learnku.com/articles/59021
❤
更多技术文章,面试干货,“xin源意码”公众号已经准备好了,关注后即可免费领取哦~