Go 垃圾回收机制学习 | 青训营

65 阅读5分钟

1. 前言

所谓垃圾就是不再需要的内存块,这些垃圾如果不清理就没办法再次被分配使用,在不支持垃圾回收的编程语言里,这些垃圾内存就是泄露的内存。

Golang的垃圾回收(GC)也是内存管理的一部分,了解垃圾回收最好先了解前面介绍的内存分配原理。

2. 垃圾回收算法

业界常见的垃圾回收算法有以下几种:

  • 引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0是回收该对象。

    • 优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。
    • 缺点:不能很好的处理循环引用,而且实时维护引用计数,有也一定的代价。
    • 代表语言:Python、PHP、Swift
  • 标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记为”被引用”,没有被标记的进行回收。

    • 优点:解决了引用计数的缺点。
    • 缺点:需要STW,即要暂时停掉程序运行。
    • 代表语言:Golang(其采用三色标记法)
  • 分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收频率。

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

3. Golang垃圾回收

早期的垃圾回收机制:

Go1.3 使用的是标记-清除法,主要步骤如下:

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

后来优化了方案将清理垃圾放到了STW之后,与程序运行同时进行,这样做减小了STW的时长,但是STW对于程序性能影响较大


Go1.5 - 三色标记法:

三色标记法分五步进行

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

缺点:将GC和程序会放一起执行,会因为cpu的调度出现下面这种情况,导致被引用的对象3会被垃圾回收掉,从而出现错误。

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

因此在此基础上拓展出了俩种方法,强三色不变式和弱三色不变式

  1. 强三色不变式:不允许黑色对象引用白色对象
  2. 弱三色不变式:黑色对象可以引用白色,白色对象存在其他灰色对象对他的引用,或者他的链路上存在灰色对象

为了实现这俩种不变式的设计思想,从而引出了屏障机制,即在程序的执行过程中加一个判断机制,满足判断机制则执行回调函数。

屏障机制分为写屏障删除屏障

值得注意的是为了保证栈的运行效率,屏障只对堆上的内存对象启用,栈上的内存会在GC结束后启用STW重新扫描。

写屏障实现的是强三色不变式,

  • 原理:对象被引用时触发的机制当白色对象被黑色对象引用时,白色对象被标记为灰色(栈上对象无插入屏障)。
  • 缺点:如果对象1在栈上新创建了一个对象6,由于栈没有屏障机制,所以对象6仍为白色节点会被回收。
  • 所以栈在GC迭代结束时(没有灰色节点),会对栈执行STW,重新进行扫描清除白色节点。(STW时间为10-100ms)

删除屏障则实现了弱三色不变式。

  • 原理:对象被删除时触发的机制。如果灰色对象引用的白色对象被删除时,那么白色对象会被标记为灰色。
  • 缺点:这种做法回收精度较低,一个对象即使被删除仍可以活过这一轮再下一轮被回收。(如果对象4没有引用对象3,此时对象3应该作为垃圾被回收,但是对象3却要等到下一轮GC才会被回收)

Go1.8 三色标记+混合写屏障

基于写屏障和删除屏障在结束时需要STW来重新扫描栈,所带来的性能瓶颈,Go在1.8引入了混合写屏障的方式实现了弱三色不变式的设计方式,混合写屏障分下面四步

  1. GC开始时将栈上可达对象全部标记为黑色(不需要二次扫描,无需STW)
  2. GC期间,任何栈上创建的新对象均为黑色
  3. 被删除引用的对象标记为灰色
  4. 被添加引用的对象标记为灰色

4. 垃圾回收触发时机

4.1 内存分配量达到阀值触发GC

每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动GC。

 阀值 = 上次GC内存分配量 * 内存增长率

内存增长率由环境变量GOGC控制,默认为100,即每当内存扩大一倍时启动GC。

4.2 定期触发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

4.3 手动触发

程序代码中也可以使用runtime.GC()来手动触发GC。这主要用于GC性能测试和统计。