..

Golang-垃圾回收

垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的内存对象,让出存储器资源。 GC过程中无需程序员手动执行。GC机制在现代很多编程语言都支持,GC能力的性能与优劣也是不同语言之间对比度指标之一。

垃圾回收算法

目前比较常见的垃圾回收算法有三种: 引用计数:为每个对象维护一个引用计数,当引用该对象的对象销毁时,引用计数 -1,当对象引用计数为 0 时回收该对象。

  • 代表语言:Python、PHP、Swift
  • 优点:对象回收快,不会出现内存耗尽或达到某个阈值时才回收。
  • 缺点:不能很好的处理循环引用,而实时维护引用计数也是有损耗的。

标记-清除:从根变量开始遍历所有引用的对象,标记引用的对象,没有被标记的进行回收。

  • 代表语言:Golang(三色标记法)
  • 优点:解决了引用计数的缺点。
  • 缺点:需要 STW,暂时停掉程序运行。

分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。

  • 代表语言:Java
  • 优点:回收性能好
  • 缺点:算法复杂

Golang 垃圾回收

Go1.3之前 标记清除法

  1. 进行 STW(stop the world 即暂停程序业务逻辑),然后从 main 函数开始找到不可达的内存占用和可达的内存占用
  2. 开始标记,程序找出可达内存占用并做标记
  3. 标记结束清除未标记的内存占用
  4. 结束 STW 停止暂停,让程序继续运行,循环该过程直到 main 生命周期结束

一开始的做法是将垃圾清理结束时才停止 STW,后来优化了方案将清理垃圾放到了 STW 之后,与程序运行同时进行,这样做减小了 STW 的时长。 但是 STW 会暂停用户逻辑对程序的性能影响是非常大的,这种粒度的 STW 对于性能较高的程序还是无法接受,但是无论怎么优化,Go V1.3都面临这个一个重要问题,就是mark-and-sweep 算法会暂停整个程序 。 因此 Go1.5 采用了三色标记法优化了 STW。

Go1.5之后 三色并发标记法

三色标记法只是为了叙述方便而抽象出来的一种说法,实际上的对象是没有三色之分的。这里的三色,对应了垃圾回收过程中对象的三种状态:

灰色:对象还在标记队列中等待 黑色:对象已被标记,gcmarkBits对应位为 1 – 该对象不会在本次 GC 中被回收 白色:对象未被标记,gcmarkBits对应位为 0 – 该对象将会在本次 GC 中被清理

三色标记法分五步进行:

  1. 将所有对象标记为白色
  2. 从根节点集合出发,将第一次遍历到的节点标记为灰色放入集合列表中
  3. 遍历灰色集合,将灰色节点遍历到的白色节点标记为灰色,并把灰色节点标记为黑色
  4. 循环这个过程
  5. 直到灰色节点集合为空,回收所有的白色节点

具体流程如下图:

回收原理: 通过上图,应该对三色标记法有了一个比较直观的了解,那么我们现在来讲讲原理。简单的讲,就是标记内存中那些还在使用中(即被引用了)的部分,而内存中不再使用(即未被引用)的部分,就是要回收的垃圾,需要将其回收,以供后续内存分配使用。上图中的 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的时间。结合了两者的优点。

混合写屏障规则

  1. GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),
  2. GC期间,任何在栈上创建的新对象,均为黑色。
  3. 被删除的对象标记为灰色。
  4. 被添加的对象标记为灰色。

触发垃圾回收的时机

主动触发:

  • 通过调用 runtime.GC 来触发 GC, 此调用阻塞式等待当前 GC 运行完毕

被动触发:

  • 定期触发GC (使用系统监控, 当超过 2 分钟没有产生任何 GC 时, 强制触发 GC)
  • 内存分配量达到阀值触发GC (堆内存的分配达到控制器计算的触发堆大小,初始大小环境变量 GOGC,之后堆内存达到上一次垃圾收集的 2 倍时才会触发 GC)

GO GC 调优

  1. 控制内存分配的速度, 限制 goroutine 的数量, 从而提高对 CPU 的利用率
  2. 减少并复用内存, 例如使用sysnc.Poll 来复用需要频繁创建的临时对象, 例如提前分配足够的内存来降低多余的复制
  3. 增大 GOGC 的值, 降低 GC 的运行频率

参考

Golang三色标记混合写屏障GC模式全分析 图解Golang垃圾回收机制! 浅析 Golang 垃圾回收机制