此篇主要记录一下常见的GC算法以及Go语言采用的垃圾回收方式。

常见的GC算法

1. 引用计数(reference counting)

引用计数算法无法处理循环引用的问题。

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

从根变量开始迭代所有被引用的对象,进行标记,最后对未被标记的变量进行清除。此算法会造成STW。为了优化这个问题,产生了三色标记算法:


1. 起初所有对象都是白色。
2. 从根出发扫描所有可达对象,标记为灰色,放入待处理队列。
3. 从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。
4. 重复步骤 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。

三色标记的一个明显好处是能够让用户程序和 mark 并发的进行。

3. 复制收集(Copy and Collection)

扫描时并不是对引用对象进行标记,而是开辟一块新的内存空间,将对象复制到新的空间中。一次扫描结束之后,所有存在于新空间的对象就是所有的非垃圾对象。

4. 分代收集(generation)

  • 根据对象的存活周期不同将内存划分为新生代和老年代,存活周期短的为新生代,存活周期长的为老年代。这样就可以根据每块内存的特点采用最适当的收集算法。
  • 对新生代进行高频小回收,然后将遗留下来的对象归为老年代,对所有对象进行低频大回收。
  • 大多数分代回收算法都采用的复制收集方法,因为小回收中垃圾的比例较大。
  • 这种方式存在一个问题:如果在某个新生代的对象中,存在老年代的对象对它的引用,它就不是垃圾了。那如何制止小回收对其回收呢?这里用到了一种叫做 写屏障(Write Barrier) 的方式。写屏障不仅用于分代收集,也用于其他GC算法中。在此算法的表现是,用一个记录集来记录从新生代到老年代的引用。

Go语言的垃圾回收

Go语言垃圾回收总体采用的是经典的mark and sweep算法。据官方说法,Go GC的基本特征是“非分代、非紧缩、写屏障、并发标记清理”。

Golang GC算法的里程碑

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

Go是一种使用了带有写屏障的、非分代的、并发标记清除的垃圾回收方式

GODEBUG=gctrace=1 go run main.go

如果在任何go run命令前面加上GODEBUG=gctrace=1,go就会打印关于垃圾回收操作的一些分析数据。

其它

  1. 在 “标记开始” 和 “标记结束” 两个阶段,都会出现STW,但在 “并发标记中” 阶段,不会出现STW。
  2. 可以通过设置GOGC变量来调整初始垃圾收集器的目标百分比值。其规则为,当新分配的数值与上一次收集后剩余的实时数值的比例达到设置的目标百分比时,就会触发GC。而GOGC的默认设置为GOGC=100,如果将其设置为GOGC=off,则可以完全禁用垃圾回收器。
  3. 一般来说,GOGC的值设置的越大,GC的频率越低,但每次GC所触发的堆内存也会越大。
  4. 在程序运行时,可以通过调用下述方法来动态调整GOGC的值:

//runtime/debug
debug.SetGCPercent

  1. 主动触发GC:可以通过runtime.GC方法来主动触发GC,也可以通过手动调用debug.FreeOSMemory方法来实现。

三色标记算法

三色标记算法(tricolor mark-and-sweep algorithm)是对传统 Mark-Sweep 算法的一个改进,它是一个支持写屏障的并发 GC 算法。其原理如下:

step 1: 创建:白、灰、黑 三个集合。

step 2: 将所有对象都放入白色集合中。

step 3: 从根节点(全局变量+全局栈+当前活跃的goroutines中的栈)开始遍历其子对象,把遍历到的对象从白色集合放入灰色集合(备注:这里放入灰色集合的都是根节点的对象)。

step 4: 然后遍历灰色集合,将灰色对象引用的对象(备注:这里指的是灰色对象引用到的所有对象,包括灰色节点间接引用的那些对象)从白色集合放入灰色集合,然后将已经分析过的灰色对象放入黑色集合中。

step 5: 循环第4步,直到灰色中无任何对象。

step 6: 通过写屏障(write-barrier)检测对象有变化,重复以上操作(备注:因为 mark 和用户程序是并行的,所以在上一步执行的时候可能会有新的对象分配,写屏障是为了解决这个问题引入的)。

step 7: 收集所有白色对象(垃圾)。

GC的时机

  1. 每次内存分配时检查当前内存分配量是否已达到阈值(环境变量GOGC):默认100%,即当内存扩大一倍时启用GC
  2. 定时触发:当最近2分钟未触发过GC时,会触发一次GC
  3. 通过runtime.GC()手动触发

GC的优化

分配的对象越多,GC性能就越差,所以需要减少对象分配的个数,比如使用 sync.Pool 进行对象复用。
注意:sync.Pool类似于缓存,其中的对象会被GC定期清理,不能存放像是数据库连接这样需要稳定存储的数据。