:mannotop Go 内存管理-垃圾回收器 – manno的博客

Go 内存管理-垃圾回收器

source article

The Go garbage collector is responsible for collecting the memory that is not in use anymore. The implemented algorithm is a concurrent tri-color mark and sweep collector. In this article, we will see in detail the marking phase, along with the usage of the different colors.

Go1.3 标记清除法

分下面四步进行

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

一开始的做法是将垃圾清理结束时才停止 STW,后来优化了方案将清理垃圾放到了 STW 之后,与程序运行同时进行,这样做减小了 STW 的时长。但是 STW 会暂停用户逻辑对程序的性能影响是非常大的,这种粒度的 STW 对于性能较高的程序还是无法接受,因此 Go1.5 采用了三色标记法优化了 STW。

Go1.5 三色标记法

三色标记算法将程序中的对象分成白色、黑色和灰色三类。

白色对象表示暂无对象引用的潜在垃圾,其内存可能会被垃圾收集器回收;

灰色对象表示活跃的对象,黑色到白色的中间状态,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;

黑色对象表示活跃的对象,包括不存在引用外部指针的对象以及从根对象可达的对象。

三色标记法分五步进行

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

这种方法看似很好,但是将 GC 和程序是一起执行的,程序执行过程中可能会更改白色对象的引用关系,导致出现下面这种情况,被引用的对象 3 受到错误的垃圾回收,程序从而出现错误。

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

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

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

屏障机制分为插入屏障和删除屏障,插入屏障实现的是强三色不变式,删除屏障则实现了弱三色不变式。值得注意的是为了保证栈的运行效率,屏障只对堆上的内存对象启用,栈上的内存会在 GC 结束后启用 STW 重新扫描。

  • 插入屏障:对象被引用时触发的机制,当白色对象被黑色对象引用时,白色对象被标记为灰色
  • 删除屏障:对象被删除时触发的机制。如果灰色对象引用的白色对象被删除时,那么白色对象会被标记为灰色。(缺点:这种做法回收精度较低,一个对象即使被删除仍可以活过这一轮再下一轮被回收)

上面的屏障保护只发生在堆的对象上。因为性能考虑,栈上的引用改变不会引起屏障触发。

所以栈在 GC 迭代结束时(没有灰色节点),会对栈执行 STW,重新进行扫描清除白色节点。(STW 时间为 10-100ms)

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

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

GC 开始时直接将栈上可达对象全部标记为黑色(不需要二次扫描,无需 STW)

GC 期间,任何栈上创建的新对象均为黑色

被删除引用的对象标记为灰色(无需判断是否被引用)

被添加引用的对象标记为灰色(无需判断是否被引用)

下面为混合写屏障过程

其它

GC的触发时机

触发 GC 有俩个条件,一是堆内存的分配达到控制器计算的触发堆大小,初始大小环境变量 GOGC,之后堆内存达到上一次垃圾收集的 2 倍时才会触发 GC。二是如果一定时间内没有触发,就会触发新的循环,该触发条件由 runtime.forcegcperiod 变量控制,默认为 2 分钟。

Go 内存管理

程序在内存上被分为堆区、栈区、全局数据区、代码段、数据区五个部分。对于 C++ 等早期编程语言栈上的内存由编译器管理回收,堆上的内存空间需要编程人员负责申请与释放。在 Go 中栈上内存仍由编译器负责管理回收,而堆上的内存由编译器和垃圾收集器负责管理回收,给编程人员带来了极大的便利性。