序言
垃圾回收(Garbage Collection,简称 GC)机制 是一种自动内存管理技术,主要用于在程序运行时自动识别并释放不再使用的内存空间,防止内存泄漏和不必要的资源浪费。这篇文章让我们来看一下 Go 语言的垃圾回收机制是如何设计的吧。
一、为什么需要垃圾回收?
熟悉 C/C++ 编程的同学肯定清楚自己手动管理内存的手段:
- 分配内存:
malloc, new
- 释放内存:
free, delete
对待内存管理一定要小心谨慎,不然稍有疏漏就会引起以下严重的问题:
- 忘记释放内存 → 内存泄漏
- 多次释放同一块内存 → 崩溃或未定义行为
- 访问已释放的内存 → 悬空指针
所以说垃圾回收机制大大的提高了内存管理的下限,但是同时这是以一定的性能开销换取的。
二、Go 垃圾回收机制
我们现在看到的机制都是比较完善的,但是很多时候我们不明白他为什么要这么做,有什么好处?这是因为我们站在巨人的肩膀上,机制是在使用中不断完善的。所以看事情不能只看现在,明白他大致来时候的路是必要的。
2.1 标记-清扫式垃圾回收(Mark-Sweep GC)
2.1.1 回收流程
首先当前执行的逻辑暂停(也称为 Stop-The-World STW
),然后标记所有存活对象。从根对象(比如全局变量、当前栈中的局部变量等)开始遍历所有被引用的对象,并将对象标记为存活状态:
现在标记的动作完成了,需要回收没有被引用的空间了。遍历整个堆的空间,然后对于没有被标记的对象,释放其内存:
最后便是恢复程序的执行了,可以看出刚开始的机制还是比较简单的。
2.1.2 Mark-Sweep 的缺点
STW
时间长降低了程序的执行的效率。如果当前的程序对于空间的申请和释放的操作比较频繁时,执行时的卡顿感会愈发的强烈,因为 STW
这段时间程序是被阻塞的,无法正常运行。
内存碎片化严重。清扫后会产生很多小块的空闲内存,可能导致大对象无法分配,降低了内存的利用率。
扫描整个堆。如果当前堆比较大的话,也会拉长 STW
的时间。
这个方式是在 Go V1.4 之前使用的,现在被替换了,但是作为一个引子还是不错的。
2.2 三色并发垃圾回收(Tri-color Concurrent GC )
2.2.1 回收流程
首先在每次创建新的对象的时候将该对象标记为 白色
:
现在触发 GC 了,从根节点遍历,将该节点 root
指向的堆对象从 white 表
放入 grey 表
,由于这里只有一个根对象,所以这里只需要处理 a
:
根上的对象遍历完成了,现在遍历 grey 表
将节点 a
指向的对象也全放入 grey 表
,同时将 a
放入 black 表
表示他的可达对象处理完成了:
之后重复第二部操作直至 grey 表
的数据为空:
到最后我们发现 white 表
只剩下了一个不可达对象 f
,这个就是需要回收的空间。纵观整个过程,其实就是一个广度遍历来查找不可达对象的过程。
比较现有的两个机制,后者一个很大优点就是 不需要遍历整个堆来查找不可达对象,因为最开始的时候就记录了创建的每一个对象。但是我们这里好像少了些什么,不需要 STW
吗?
2.2.2 假设没有 STW
就比如 e
其实是一个可达对象的,但是由于在执行回收的过程中当前的程序也在正常的执行,让 d
和 e
断开连接并让一个新的根节点(比如局部变量)指向 e
:
因为对根节点的遍历只在最初执行一遍,后续不会再遍历了导致错误地判定 e
为不可达对象释放该空间,这不就错误地释放空间了吗(也称为 漏标
)。
最直接的方式就是在回收的过程中加上 STW
,但是这个方式的弊端上面也说过了。那怎么办呢?减少 STW
的时间。
2.2.3 屏障机制
1. 强弱三色不变式
由于程序的执行和垃圾回收的过程是并发的,就导致了错误地回收了某些还需要继续使用的对象。为了避免这种情况,引入了 三色不变式。
强三色不变式,所有黑对象不能直接或间接引用白对象。也就是说:如果一个对象已经是黑色,它不能指向任何未被标记的白色对象。
弱色不变式。所有白对象可以被黑色对象引用,但是这个白色对象必须存在着其他灰色对象对他的引用。
2. 插入写屏障
满足:强三色不变式
操作:当一个黑色对象引用一个白色对象之前,先将该白色对象修改为灰色对象,在建立引用:
并且这里还有一个机制是 插入写屏障只作用于堆对象。因为栈上的变量变更比较频繁, 如果一变更我们就去执行插入写屏障会非常的耽误时间。作为补偿,会在整体三色标记清除之后,专门对栈上的空间执行次三色标记扫描并加上 STW
保护。
3. 删除写屏障
满足:弱三色不变式
操作:当一个白色对象被上游删除引用时,会将将自己修改为灰色对象:
这种方式其实也是延迟回收策略,当真正想删除该对象时,这一轮他会存活下来,但是下一轮肯定会被带走。
2.3.4 Tri-color Concurrent 的缺点
这两种方式任选一种即可解决漏标的问题,Go V1.8 及以前使用的是删除写屏障。该种方式的缺点是:
- 回收精度偏低。本次 GC 过程中需要删除的对象会在下一轮清除
而插入写屏障的缺点也不小:
- 会在结束时扫描整个栈,并且伴随着
STW
那是否可以取长补短互相融合呢?
2.3 混合写屏障机制(Hybrid Write Barrier)
将栈上的对象扫描之后全部标记为 黑色,期间任何新增的对象标记为 灰色,任何被删除的对象也标记为 灰色。这样节省了扫描整个栈并伴随的 STW
带来的性能消耗。
三、总结
现在纵观大体的发展路线,你是否可以理解:混合写屏障(Hybrid Write Barrier)是一种改进型写屏障机制,它结合了 删除写屏障 和 插入写屏障 的优点,在并发三色标记中有效地防止漏标问题,并显著减少了 STW 时间。