Go垃圾回收机制

前言

垃圾即为不再需要的内存块。如果这些垃圾不清理,无法再次被分配使用。在不支持垃圾回收(GC)的编程语言中,这些垃圾内存就会泄露。Golang的垃圾回收也是内存管理的一部分。了解垃圾回收,最好先了解内存分配原理。

垃圾回收算法

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

引用计数

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

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

标记-清除

从根变量开始遍历所有引用的对象,引用的对象标记为“被引用”,没有被标记的进行回收。

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

分代收集

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

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

简单来说,垃圾回收的核心是标记出哪些内存还在使用中(即被引用到),哪些内存不再使用(即未被引用),并回收未被引用的内存,以供后续内存分配时使用。

Golang垃圾回收

1、垃圾回收原理

下图展示了一段内存,内存中既有已分配的内存,也有未分配的内存。垃圾回收的目标是找到那些已经分配但没有对象引用的内存,并回收掉。

内存回收原理

上图中,内存块1、2、4号位上的内存块已被分配(数字1代表已被分配,0代表未分配)。变量a, b为指针,指向内存的1、2号位。内存块的4号位曾经被使用过,但现在没有任何对象引用,需要被回收。

垃圾回收开始时从root对象开始扫描,把root对象引用的内存标记为“被引用”。由于内存块中存放的可能是指针,所以还需要递归地进行标记,全部标记完成后,只保留被标记的内存,未被标记的全部标识为未分配,完成回收。

3.2 内存标记(Mark)

在内存分配时,span数据结构中维护了一个个内存块,并由位图allocBits表示每个内存块的分配情况。在span数据结构中还有另一个位图gcmarkBits用于标记内存块的引用情况。

内存标记

如上图所示,allocBits记录了每块内存分配情况,gcmarkBits记录了每块内存标记情况。标记阶段对每块内存进行标记,有对象引用的内存标记为1(图中灰色所示),没有引用的保持默认为0。

3.3 三色标记法

三色标记法对应了垃圾回收过程中对象的三种状态:

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

例如,当前内存中有A~F共6个对象,根对象a, b分别引用了对象A、B,而B对象又引用了对象D。GC开始前各对象状态如下图所示:

GC初始状态

接着开始扫描根对象a、b:

GC扫描

由于根对象引用了对象A、B,那么A、B变为灰色对象。接下来分析灰色对象,分析A时,A没有引用其他对象,很快转入黑色;B引用了D,则B转入黑色的同时还需要将D转为灰色,进行接下来的分析。

GC标记过程

最终,所有黑色对象被保留下来,白色对象被回收。

3.4 Stop The World

Golang中的STW(Stop The World)是指停掉所有的goroutine,专心进行垃圾回收,待回收结束后再恢复goroutine。STW时间的长短直接影响应用执行,时间过长对一些web应用来说是不可接受的,这也是广受诟病的原因之一。为了缩短STW时间,Golang不断优化垃圾回收算法,很大程度上改善了这个问题。

3.5 写屏障(Write Barrier)

写屏障让goroutine与GC同时运行,虽然不能完全消除STW,但可以大大减少STW时间。在GC特定时机开启写屏障,指针传递时会把指针标记,即本轮不回收,下次GC时再确定。

3.6 Mutator Assist

GC过程中,如果goroutine需要分配内存,那么这个goroutine会参与部分GC工作,帮助GC做一部分工作,这个机制叫作Mutator Assist。

3.7 内存分配阀值

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

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

内存增长率由环境变量GOGC控制,默认为100,即每当内存扩大一倍时启动GC。默认情况下,最长2分钟触发一次GC,这个间隔在 src/runtime/proc.go:forcegcperiod 变量中被声明。

3.8 手动GC

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

GC性能优化

GC性能与对象数量负相关,对象越多GC性能越差,对程序影响越大。因此,GC性能优化的思路之一是减少对象分配个数,比如对象复用或使用大对象组合多个小对象等。此外,由于内存逃逸现象,也可能产生隐式的内存分配,增加GC负担。

通过了解Golang的垃圾回收机制,可以更好地优化代码并提高程序性能。

打 赏