【Go】Go垃圾回收算法

一. 什么是垃圾回收

曾几何时,内存管理是程序员开发应用的一大难题。传统的系统级编程语言(主要指C/C++)中,程序员必须对内存小心的进行管理操作,控制内存的申请及释放。稍有不慎,就可能产生内存泄露问题,这种问题不易发现并且难以定位,一直成为困扰开发者的噩梦。如何解决这个头疼的问题呢?

过去一般采用两种办法:

  1. 内存泄露检测工具。这种工具的原理一般是静态代码扫描,通过扫描程序检测可能出现内存泄露的代码段。然而检测工具难免有疏漏和不足,只能起到辅助作用。(Valgrind)
  2. 智能指针。这是c++中引入的自动内存管理方法,通过拥有自动内存管理功能的指针对象来引用对象,是程序员不用太关注内存的释放,而达到内存自动释放的目的。这种方法是采用最广泛的做法,但是对程序员有一定的学习成本(并非语言层面的原生支持),而且一旦有忘记使用的场景依然无法避免内存泄露。

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

二. 常见的垃圾回收算法

① 引用计数

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

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

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

② 标记清除

该方法分为两步,标记从根变量开始迭代得遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;标记完成后进行清除操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。

这种方法解决了引用计数的不足,但是也有比较明显的问题:每次启动垃圾回收都会暂停当前所有的正常代码执行,回收是系统响应能力大大降低!当然后续也出现了很多mark&sweep算法的变种(如三色标记法)优化了这个问题。

③ 分代收集

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

三. Go的GC工作机制

Go的GC自打一开始就被很多人诟病,经过这么多年的发展Go的GC已经变得非常的优秀了,以下是Go的GC算法里程碑

  1. v1.1 STW (stop the world)
  2. v1.3 Mark STW, Sweep 并行
  3. v1.5 三色标记法
  4. v1.8 hybrid write barrier

Go语言GC算法主要是基于Mark and Sweep (标记清除)算法,在此基础上进行改进和优化。

① Mark and Sweep

Mark and Sweep(标记清除)算法主要是以下2个步骤

  1. 标记(Mark): 找出所有不可达对象,然后做上标记
  2. 清除(Sweep): 回收标记好的对象

我们通过以下图解来解释 标记清除 算法是如何工作的

  1. 开始标记,程序暂停,此时程序和对象的关系如下图所示
  2. 找到所有可达对象,并做上标记
  3. 标记完成后开始清除未标记的对象
  4. 清除完成后,对象如下图所示

标记清除算法存在以下几个问题

  1. STW (stop the world) 标记对象的时候程序需要暂停,导致程序出现卡顿 (最主要的问题)
  2. 标记需要扫描整个堆
  3. 清除对象会产生堆碎片

STW指的是runtime把所有的协程都冻结了,意味着用户逻辑是暂停的,这样所有的对象都不会被修改,这个时候去扫描是绝对安全的。

② Tri-color Marking

为了解决标记清除算法带来的问题,Go在标记清除算法基础上提出来Tri-color Marking(三色标记法)算法来优化GC过程,大体流程如下

  1. 最开始所有的对象都是白色
  2. GC开始,扫描所有可达的对象,标记为灰色
  3. 从灰色对象中找到其引用对象并标记为灰色,自己标记为黑色
  4. 监控对象修改,循环步骤3,直到没有任何灰色对象
  5. GC回收白色对象
  6. 最后把所有黑色对象变成白色

三色标记法是怎么优化STW问题的呢,主要是以下2点

  1. 标记操作和用户逻辑并行: 用户逻辑经常会生成或改变对象引用,那如何保证标记和用户逻辑并行呢?Go为了解决这个问题引入了写屏障机制,在GC的过程中会监控对象的内存修改,并对对象进行重新标记,这个时候用户逻辑也可以执行 (实际上是很短暂的STW,然后对对象重新标记),所以标记操作可以做到一定程度和用户逻辑并行。
  2. 清除操作和用户逻辑并行: 我们知道三色标记法中最后只剩下的黑白两种对象,黑色对象是程序恢复后接着使用的对象,如果不碰触黑色对象,只清除白色的对象,肯定不会影响程序逻辑,所以清除白色对象和用户逻辑可以并行。

通过允许用户逻辑在标记和清除操作上做到并行处理来缩短STW的时间,提升整体GC的性能。

你可能感兴趣的:(17.,Go)