Go - 垃圾回收
一、前言
所谓垃圾就是不再需要的内存块,这些垃圾如果不清理就没办法再次被分配使用,在不支持垃圾回收的编程语言里,这些垃圾内存就是泄露的内存。
Golang的垃圾回收(GC)也是内存管理的一部分,了解垃圾回收最好先了解前面介绍的内存分配原理。
二、垃圾回收算法
业界常见的垃圾回收算法有以下几种:
引用计数: 对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0时回收该对象。
- 优点:对象可以很快地被回收,不会出现内存耗尽或达到某个阀值时才回收。
- 缺点:不能很好地处理循环引用,而且实时维护引用计数,也有一定的代价。
- 代表语言:Python、PHP、Swift
标记-清除: 从根变量开始遍历所有引用的对象,引用的对象标记为”被引用”,没有被标记的进行回收。
- 优点:解决了引用计数的缺点。
- 缺点:需要STW,即要暂时停掉程序运行。
- 代表语言:Golang(其采用三色标记法)
分代收集: 按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不同的回收算法和回收频率。
- 优点:回收性能好
- 缺点:算法复杂
- 代表语言: JAVA
三、Golang垃圾回收
3.1 Golang垃圾回收的原理
简单的说,垃圾回收的核心就是标记出哪些内存还在使用中(即被引用到),哪些内存不再使用了(即未被引用),把未被引用的内存回收掉,以供后续内存分配时使用。
下图展示了一段内存:内存中既有已分配掉的内存,也有未分配的内存,垃圾回收的目标就是把那些已经分配的但没有对象引用的内存找出来并回收掉:
上图中:内存块1、2、4号位上的内存块已被分配(数字1代表已被分配,0 未分配)。变量a, b为一指针,指向内存的1、2号位。内存块的4号位曾经被使用过,但现在没有任何对象引用了,就需要被回收掉。
3.2 内存标记Mark
前面介绍内存分配时介绍过span数据结构,span中维护了一个个内存块,并由一个位图allocBits表示每个内存块的分配情况。在span数据结构中还有另一个位图gcmarkBits用于标记内存块被引用情况。
如上图所示,allocBits记录了每块内存分配情况,而gcmarkBits记录了每块内存标记情况。
-
标记阶段对每块内存进行标记,有对象引用的的内存标记为1(如图中灰色所示),没有引用到的保持默认为0.
-
allocBits和gcmarkBits数据结构是完全一样的,标记结束就是内存回收,回收时将allocBits指向gcmarkBits,则代表标记过的才是存活的,gcmarkBits则会在下次标记时重新分配内存,非常的巧妙。
3.3 三色标记法
三色只是为了叙述上方便抽象出来的一种说法,实际上对象并没有颜色之分。
这里的三色,对应了垃圾回收过程中对象的三种状态:
- 灰色:对象还在标记队列中等待
- 黑色:对象已被标记,gcmarkBits对应的位为1(该对象不会在本次GC中被清理)
- 白色:对象未被标记,gcmarkBits对应的位为0(该对象将会在本次GC中被清理)
示例: 当前内存中有A~F一共6个对象。
根对象a,b本身为栈上分配的局部变量,根对象a、b分别引用了对象A、B, 而B对象又引用了对象D
- 初始状态下所有对象都是白色
- 开始扫描根对象a、b
由于根对象引用了对象A、B,那么A、B变为灰色对象
- 开始分析灰色对象
A没有引用其他对象很快就转入黑色,B引用了D,则B转入黑色的同时还需要将D转为灰色.
- 灰色对象只有D,由于D没有引用其他对象,所以D转入黑色。标记过程结束
黑色的对象会被保留下来,白色对象会被回收掉
3.4 STW
对于垃圾回收来说,回收过程中也需要控制住内存的变化,否则回收过程中指针传递会引起内存引用关系变化,如果错误的回收了还在使用的内存,结果将是灾难性的。
-
Golang中的STW(Stop The World)就是停掉所有的goroutine,专心做垃圾回收,待垃圾回收结束后再恢复goroutine。
-
STW时间的长短直接影响了应用的执行,时间过长对于一些web应用来说是不可接受的,这也是广受诟病的原因之一。
-
为了缩短STW的时间,Golang不断优化垃圾回收算法,这种情况得到了很大的改善。
四、垃圾回收的优化
前面说过STW目的是防止GC扫描时内存变化而停掉goroutine,而写屏障就是让goroutine与GC同时运行的手段。虽然写屏障不能完全消除STW,但是可以大大减少STW的时间。
4.1 强/弱三色不变
在讲屏障机制之前,采用三色标记法在不使用STW的时候,那么有可能会出现以下情况:
- 强三色不变式:强制性不允许黑色对象引用白色对象;
- 弱三色不变时:在三色标记法进行标记时,允许黑色对象引用白色对象,但是白色对象必须存在其他灰色对象对它的引用,或者有灰色对象对该白色对象是可达的。
4.2 屏障机制
三色标记法中实现强弱三色不变式,就必须要加入一些额外的操作。在golang中主要是依靠屏障机制来实现三色不变式。在对变量进行赋值的时候编译器自动插入额外的操作。
由于栈对速度的要求比较高,所以栈上不启用屏障机制。
4.2.1 插入写入屏障
插入写屏障——对象被引用时触发的机制 ,插入写屏障满足的是强三色不变式。
插入写屏障实现的是:
- 不管是黑色对象还是白色对象引用其它对象,都会把被引用的对象从白色对象变为灰色对象。
- 如果被引用的对象是灰色或者黑色则不处理。
为了保证栈的速度,如果栈上的对象增加插入屏障,会大幅度增加引用对象的性能,所以栈空间不触发插入屏障,堆空间会触发插入屏障。
在回收白色之前会重新遍历扫描一次栈空间,此时添加STW暂停来保护栈,防止外界干扰(有新的白色被黑色添加);
插入屏障的缺点:在结束的时候需要开启STW重新扫描栈,但是所需时间较短(10-100ms);
4.2.2 删除屏障
删除写屏障——对象被删除时出发的机制
被删除的对象如果为灰色或者白色,那么被标记为灰色(满足弱三色不变式)。
说明:删除相当于用nil来替换被删除的对象,将被删除的对象置为灰,此时该对象即使被删除也会被暂时保留下来,避免该对象被其他的对象使用
删除屏障缺点: 对象断开的被引用对象都会标记为灰色对象,这就意味着,这些对象中的垃圾也能存活到下一次GC开始前。所以删除写屏障的浮动垃圾会多一些。
4.2.3 混合写屏障
在golang v1.8中,综合了插入写屏障和删除写屏障,创造了一种新的写屏障,这种写屏障就被称为混合写屏障,将STW的时间控制在50us以下。
混合写屏障解决:插入写屏障扫描内存完成后,需要额外重新标记栈区对象,并対栈区对象及其可达的对象进行可达性分析的缺点 。 删除写屏障需要在标记开始时,就必须把栈上的对象标记为黑色对象,并将其引用的对象标记为灰色的缺点。
- GC开始将栈上的对象全部扫描并标记为黑色;
- GC期间,任何在栈上创建的新对象,均为黑色(避免进行二次扫描);
- 被删除的对象标记为灰色;
- 被添加的对象标记为灰色;
在Go使用的混合写屏障中是不会对栈空间进行写屏障的,栈上的对象在一开始就全部标记为黑色,并且在GC过程中新创建的对象也是黑色的
4.3 辅助GC
为了防止内存分配过快,在GC执行过程中,如果goroutine需要分配内存,那么这个goroutine会参与一部分GC的工作,即帮助GC做一部分工作,这个机制叫作Mutator Assist。
五、垃圾回收触发的时机
- 内存分配量达到阀值触发GC
每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动GC。
阀值 = 上次GC内存分配量 * 内存增长率
内存增长率由环境变量GOGC控制,默认为100,即每当内存扩大一倍时启动GC。
- 定期触发GC
默认情况下,最长2分钟触发一次GC,这个间隔在src/runtime/proc.go:forcegcperiod变量中被声明:
// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
//
// This is a variable for testing purposes. It normally doesn't change.
var forcegcperiod int64 = 2 * 60 * 1e9
-
手动触发
程序代码中也可以使用
runtime.GC()来手动触发GC。这主要用于GC性能测试和统计。