Golang-垃圾回收
垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的内存对象,让出存储器资源。 GC过程中无需程序员手动执行。GC机制在现代很多编程语言都支持,GC能力的性能与优劣也是不同语言之间对比度指标之一。
垃圾回收算法
目前比较常见的垃圾回收算法有三种: 引用计数:为每个对象维护一个引用计数,当引用该对象的对象销毁时,引用计数 -1,当对象引用计数为 0 时回收该对象。
- 代表语言:Python、PHP、Swift
- 优点:对象回收快,不会出现内存耗尽或达到某个阈值时才回收。
- 缺点:不能很好的处理循环引用,而实时维护引用计数也是有损耗的。
标记-清除:从根变量开始遍历所有引用的对象,标记引用的对象,没有被标记的进行回收。
- 代表语言:Golang(三色标记法)
- 优点:解决了引用计数的缺点。
- 缺点:需要 STW,暂时停掉程序运行。
分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。
- 代表语言:Java
- 优点:回收性能好
- 缺点:算法复杂
Golang 垃圾回收
Go1.3之前 标记清除法
- 进行 STW(stop the world 即暂停程序业务逻辑),然后从 main 函数开始找到不可达的内存占用和可达的内存占用
- 开始标记,程序找出可达内存占用并做标记
- 标记结束清除未标记的内存占用
- 结束 STW 停止暂停,让程序继续运行,循环该过程直到 main 生命周期结束
一开始的做法是将垃圾清理结束时才停止 STW,后来优化了方案将清理垃圾放到了 STW 之后,与程序运行同时进行,这样做减小了 STW 的时长。 但是 STW 会暂停用户逻辑对程序的性能影响是非常大的,这种粒度的 STW 对于性能较高的程序还是无法接受,但是无论怎么优化,Go V1.3都面临这个一个重要问题,就是mark-and-sweep 算法会暂停整个程序 。 因此 Go1.5 采用了三色标记法优化了 STW。
Go1.5之后 三色并发标记法
三色标记法只是为了叙述方便而抽象出来的一种说法,实际上的对象是没有三色之分的。这里的三色,对应了垃圾回收过程中对象的三种状态:
灰色:对象还在标记队列中等待 黑色:对象已被标记,gcmarkBits对应位为 1 – 该对象不会在本次 GC 中被回收 白色:对象未被标记,gcmarkBits对应位为 0 – 该对象将会在本次 GC 中被清理
三色标记法分五步进行:
- 将所有对象标记为白色
- 从根节点集合出发,将第一次遍历到的节点标记为灰色放入集合列表中
- 遍历灰色集合,将灰色节点遍历到的白色节点标记为灰色,并把灰色节点标记为黑色
- 循环这个过程
- 直到灰色节点集合为空,回收所有的白色节点
具体流程如下图:
回收原理: 通过上图,应该对三色标记法有了一个比较直观的了解,那么我们现在来讲讲原理。简单的讲,就是标记内存中那些还在使用中(即被引用了)的部分,而内存中不再使用(即未被引用)的部分,就是要回收的垃圾,需要将其回收,以供后续内存分配使用。上图中的 A、B、D 就是被引用正在使用的内存,而C、F、E 曾经被使用过,但现在没有任何对象引用,就需要被回收掉。
三色标记法缺点 将 GC 和程序会放一起执行,会因为 cpu 的调度出现下面这种情况,导致被引用的对象 3 却被GC给“误杀”回收掉了,从而出现错误。
可以看出,有两种情况,在三色标记法中,是不希望被发生的。
- 条件1: 一个白色对象被黑色对象引用(白色被挂在黑色下)
- 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色)
如果当以上两个条件同时满足时,就会出现对象丢失现象!
屏障机制 强三色不变式和弱三色不变式
- 强三色不变式
强三色不变色实际上是强制性的不允许黑色对象引用白色对象,这样就不会出现有白色对象被误删的情况。
- 弱三色不变式
弱三色不变式强调,黑色对象可以引用白色对象,但是这个白色对象必须存在其他灰色对象对它的引用,或者可达它的链路上游存在灰色对象。 这样实则是黑色对象引用白色对象,白色对象处于一个危险被删除的状态,但是上游灰色对象的引用,可以保护该白色对象,使其安全。
插入屏障和删除屏障
为了遵循上述的两个方式,GC算法演进到两种屏障方式,他们“插入屏障”, “删除屏障”。
- 插入屏障 对象被引用时触发的机制,当白色对象被黑色对象引用时,白色对象被标记为灰色(栈上对象无插入屏障)
缺点在于:如果对象 1 在栈上新创建了一个对象 6,由于栈没有屏障机制,所以对象 6 仍为白色节点会被回收 栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用, 所以**“插入屏障”机制,在栈空间的对象操作中不使用. 而仅仅使用在堆空间对象的操作中**.
所以栈在 GC 迭代结束时(没有灰色节点),会对栈执行 STW,重新进行扫描清除白色节点。(STW 时间为 10-100ms)
- 删除屏障 对象被删除时触发的机制。如果灰色对象引用的白色对象被删除时,那么白色对象会被标记为灰色。
缺点在于:这种做法回收精度较低,一个对象即使被删除仍可以活过这一轮再下一轮被回收。(如果对象 4 没有引用对象 3,此时对象 3 应该作为垃圾被回收,但是对象 3 却要等到下一轮 GC 才会被回收) 同样也存在对栈的二次扫描影响程序的效率。
Go1.8 三色标记 + 混合写屏障(hybrid write barrier)机制
插入写屏障和删除写屏障的短板:
- 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;
- 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。 Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。
混合写屏障规则
- GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),
- GC期间,任何在栈上创建的新对象,均为黑色。
- 被删除的对象标记为灰色。
- 被添加的对象标记为灰色。
触发垃圾回收的时机
主动触发:
- 通过调用
runtime.GC
来触发 GC, 此调用阻塞式等待当前 GC 运行完毕
被动触发:
- 定期触发GC (使用系统监控, 当超过 2 分钟没有产生任何 GC 时, 强制触发 GC)
- 内存分配量达到阀值触发GC (堆内存的分配达到控制器计算的触发堆大小,初始大小环境变量 GOGC,之后堆内存达到上一次垃圾收集的 2 倍时才会触发 GC)
GO GC 调优
- 控制内存分配的速度, 限制
goroutine
的数量, 从而提高对 CPU 的利用率 - 减少并复用内存, 例如使用
sysnc.Poll
来复用需要频繁创建的临时对象, 例如提前分配足够的内存来降低多余的复制 - 增大
GOGC
的值, 降低 GC 的运行频率