Golang-GC垃圾回收机制

C/C++程序员必须对内存小心的进行管理操作,控制内存的申请及释放。这种问题不易发现并且难以定位,过去一般采用两种办法解决这个头疼的问题:

  • 内存泄露检测工具。这种工具的原理一般是静态代码扫描,通过扫描程序来检测可能出现内存泄露的代码段。然而检测工具难免有疏漏和不足,只能起到辅助作用。
  • 智能指针。这是c++中引入的自动内存管理方法,通过拥有自动内存管理功能的指针对象来引用对象,使程序员不用太关注内存的释放,而达到内存自动释放的目的(并非语言层面的原生支持)。

为了解决这个问题,后来开发出来的几乎所有新语言(java,python,php等等)都引入了语言层面的自动内存管理 – 也就是语言的使用者只用关注内存的申请而不必关心内存的释放,内存释放由虚拟机(virtual machine)或运行时(runtime)来自动进行管理。而这种对不再使用的内存资源进行自动回收的行为就被称为垃圾回收。

常见的垃圾回收方法

1. 引用计数 (reference counting)

这是最简单的一种垃圾回收算法,和之前提到的智能指针异曲同工。对每个对象维护一个引用计数,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被创建或被赋值给其他对象时引用计数自动加一。当引用计数为0时则立即回收对象。通俗来讲就是给程序中的对象都赋予一个计数器,对象被实例化一次则计数器自动加一,对象的实例被销毁或更新时的引用计数自动减一

这种方法的优点是实现简单,并且内存的回收很及时。这种算法在内存比较紧张和实时性比较高的系统中使用的比较广泛,如ios cocoa框架,php,python等。简单引用计数算法也有明显的缺点:

  • 频繁更新引用计数降低了性能。一种简单的解决方法就是编译器将相邻的引用计数更新操作合并到一次更新;还有一种方法是针对频繁发生的临时变量引用不进行计数,而是在引用达到0时通过扫描堆栈确认是否还有临时对象引用而决定是否释放。等等还有很多其他方法,具体可以参考这里。
  • 循环引用问题。当对象间发生循环引用时引用链中的对象都无法得到释放。最明显的解决办法是避免产生循环引用,如cocoa引入了strong指针和weak指针两种指针类型。或者系统检测循环引用并主动打破循环链。当然这也增加了垃圾回收的复杂度。

2. 标记-清除 (mark and sweep)

该方法分为两步:第一步标记从根变量开始,通过迭代来遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;第二步标记完成后进行清除操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。通俗来讲就是当堆中有效内存空间被消耗完,挂起正常程序来进行两项工作(标记和清除)。遍历根对象(根搜索算法),将能够从根对象遍历到的对象打上 “被引用” 的标记,对没有标记上的对象进行内存回收。内存回收后唤醒主程序。至于为何要挂起主程序运行再来开启GC线程,原因很简单,试想一下主线程刚实例化一个变量还没标记,就遭到了GC线程的清洗。这种方法解决了引用计数的不足,但是也有比较明显的问题:

  • 每次启动垃圾回收都会暂停当前所有的正常代码执行,回收时系统响应能力大大降低!当然后续也出现了很多mark&sweep算法的变种(如三色标记法)优化了这个问题。

3. 分代收集 (generation)

经过大量实际观察得知,在面向对象编程语言中,绝大多数对象的生命周期都非常短。分代收集的基本思想是,将堆划分为两个或多个称为 代(generation)的空间。新创建的对象存放在称为 新生代(young generation)中(一般来说,新生代的大小会比 老年代小很多),随着垃圾回收的重复执行,生命周期较长的对象会被 提升(promotion)到老年代中。因此,新生代垃圾回收和老年代垃圾回收两种不同的垃圾回收方式应运而生,分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度非常快,比老年代快几个数量级,即使新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是因为大多数对象的生命周期都很短,根本无需提升到老年代。通俗来讲就是根据对象存活周期的不同,将被长期被使用的对象划分到老年代,短暂使用的对象划分到新生代,然后对不同的代的空间采用不同的回收算法。对新生代的空间进行快速回收,回收若干次后,新生代中某些早期存在对象仍然存在,则将这些对象踢入老年代,再进过若干次回收,老年代中空间被用完,则进行一次全量回收(Full GC)

Golang中的垃圾回收器

Golang 从第一个版本以来,GC 一直是大家诟病最多的。但是每一个版本的发布基本都伴随着 GC 的改进。下面列出一些比较重要的改动。

  • v1.1 传统 Mark-Sweep
  • v1.3 Mark STW, Sweep 并行
  • v1.5 三色标记法
  • v1.8 hybrid write barrier

触发GC机制

  1. 在申请内存的时候,检查当前当前已分配的内存是否大于上次GC后的内存的2倍,若是则触发 (主GC线程为当前M,内核线程) MPG模式的CSP实现
  2. 监控线程发现上次GC的时间已经超过两分钟了则触发;将一个G任务放到全局G队列中去。(主GC线程为执行这个G任务的M)

Mark STW, Sweep

go语言1.3版本垃圾回收总体采用的是经典的**标记-清理 (Mark-And-Sweep)**算法。就是先标记需要回收的内存对象块,然后清理掉。在这个过程中存在一个 stop the world 的问题,会中断用户逻辑。

  1. stop the world,等待所有的M休眠;此时所有的业务逻辑代码都停止
  2. 标记:分配gc标记任务,唤醒 gcproc个 M(就是第一步休眠的那些),全部用来做标记,直到所有的M都做完,才结束;并且所有M再次进入休眠
  3. 清理:有一个单独的goroutine去清理已经标记的内存对象快
  4. start the world,设置gcwaiting=0,唤醒所有的M(不会超过P个数)

三色标记法

三色标记法是传统 Mark-Sweep 的一个改进,它是一个并发的 GC 算法。在程序执行的同时进行收集,并不需要暂停整个程序。但是也会有一个缺陷,可能程序中的垃圾产生的速度会大于垃圾收集的速度,这样会导致程序中的垃圾越来越多无法被收集掉

  1. 首先创建三个集合:白、灰、黑。
  2. 将所有对象放入白色集合中。
  3. 然后从根节点开始遍历所有一级对象(注意这里并不递归遍历),把遍历到的对象从白色集合放入灰色集合。
  4. 之后遍历灰色集合,将灰色对象引用的下一级对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
  5. 重复 4 直到灰色中无任何对象
  6. 通过write-barrier检测对象有变化,重复以上操作
  7. 收集所有白色对象(垃圾)

强弱三色不变式

一般的三色标记法,没有STW过程保护的话,其最不希望发生的事情:

  1. 一个白色对象被黑色对象引用
  2. 灰色对象白色对象之间的可达关系遭到破坏

这样将会导致部分白色对象被GC当作垃圾回收。

强三色不变式

黑色对象强制性的不允许引用白色对象(破坏事件1)

弱三色不变式

黑色对象可以引用白色对象,但是被引用的白色对象必须有其他的上游对象为灰色的引用(破坏事件2)

屏障机制

屏障机制类似于 hook 机制,是在操作某件事前做一些判断

插入写屏障

对象被引用时,触发的机制。 具体操作:当A对象引用B对象时,B对象被标记为灰色(将B挂在A下游,B必须被标记为灰色),满足强三色不变式(不存在黑色对象对白色对象的引用,因为白色对象会被强制变灰色) 不足:结束时需要STW来重新扫描栈,大约需要10-100ms

删除写屏障

对象被删除时,触发的机制 具体操作:被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。满足弱三色不变式(保护灰色对象到白色对象的路径不会断) 不足:回收精度低,有些对象即使被删除了对象引用,依然可以活过这一轮的GC。

混合写屏障

具体操作:

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

满足:变形的弱三色不变式.(结合和插入删除写屏障的优点)

总结

“非分代的、非紧缩、写屏障、并发标记清理”

并发清理: 垃圾回收(清理过程)与用户逻辑并发执行

三色并发标记 : 标记与用户逻辑并发执行

参考

golang 垃圾回收 gc golang的垃圾回收(GC)机制 Golang垃圾回收机制 go笔记-GC eliminate-rescan