跳到主要内容

Go的GC机制

一 Go垃圾回收机制概述

C++使用指针引用计数方式来回收对象,但是该方式不能处理循环引用!所以之后的GC算法进行了改进,出现了:标记清理、节点复制、分代收集等算法。

Go语言的垃圾回收使用标记清理算法,将需要的内存块进行标记(mark),没有标记的内存块将会被清理(sweep)。Go用到的策略有:

  • 并发标记和清理(concurrent mark and sweep)
  • 写屏障(write barrier)
  • 非分代(non-generational)
  • 非紧缩(non-compacting))

二 标记清理算法

2.0 标记清理算法简介

标记清理算法中包含2个区域:

  • 标记初始的root区:程序运行到当前时刻的栈和全局数据区域
  • 受控堆区:该区域大多数据都是以后不会被用到的,将会被当做垃圾进行回收

判断一个对象是否是垃圾,需要看这个对象是否被当前栈或全局数据区域(root区域)的对象直接或间接地引用 。 如果没有任何对象引用到它,则说明它没有被使用,因此可以安全地当作垃圾回收。

标记清理算法分为两阶段:

  • 标记阶段:从 root 区域出发,扫描所有 root 区域的对象直接或间接引用到的对象,将这些对象全部加上标记
  • 清理阶段:扫描整个堆区,对所有无标记的对象进行回收

Go在1.5之前,垃圾回收的标记和清理都是STW(Stop The World),即要停止所有的 goroutine ,以此来保证已经被标记的区域不会被再次修改 引用关系,造成清理错误。这样做,每次标记都要STW,效率极低。

Go对GC算法的改进,即避免STW:

  • 标记阶段:1.5版本开始,使用三色标记法实现节点的并发
  • 清理阶段:1.8版本开始,加入混合写屏障(hybrid write barrier),使GC达到了毫秒级以下

这里会引 出几个问题:怎样实现井发标记?标记记录在哪里?怎样知道对象是否被引 用?什么时候触发清理动作?回收时进程怎么办?

2.1 三色标记法

三色标记的步骤是:

  • 1、最开始所有对象都是白色
  • 2、扫描所有可达对象,标记为灰色,放入待处理队列;
  • 3、从队列里提取灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色
  • 4、写屏障,监控对象内存修改,重新标色或放入队列
  • 5、完成标记后对象不是白色就是黑色,清理操作只需把白色对象回收即可。

2.2 并发标记

所谓并发标记:

  • 一是指通过 write-barrier (写屏障)能够与用户代码并发进行
  • 二是指通过 gc-work 队列实现非递归地标记可达对象,换而言之标记工作不是递归进行的,而是多个 goroutine 并发进行的

贴士:用户程序会一直修改内存,而此时又使用与用户程序并发运行的垃圾回收算法,就需要写屏障。当发现对象己经标记为黑色了,但该对象引用的对象却变了,那么把后来引用的对象变灰入队,原来的被引用对象保持灰色不变。这个 write barrier 是编译器在每一处内存写操作前生成一小段代码来做的。

并发标记要实现非递归地遍历标记可达节点,就需要一个队列。这个队列还可以有助于区分黑色对象和灰色对象,因为标记位只有一个。在队列中的标记是灰色对象,标记了但是不在队列中的是黑色对象,未标记的是白色对象。

实现源码位于函数 gcDrain()scanobject()greyobject()

2.3 标记位

Go将标记位存放在bitmap区域,该区域每个字对应4位标记位

2.4 清理触发

如果频繁垃圾回收会导致CPU的浪费,如果回收启动太晚,则会导致堆内存累计太多,所以需要合理设计垃圾回收的触发条件。
每一次 mallocgc 都会检查是否需要 gcsta扰,触发条件由两个参数决定: gc一trigger 和 gcpercent。

gc_trigger 初始为 4MB, next_gc 初始为 4MB ,之后每次标记完成时将重新计算动态调整值大小 。但 gc_trigger 至少要大于初始的 4MB ,同时至少要比当前使用的 heap 大 1MB才会触发 GC 操作。

这个检查是在堆上分配大于 32KB 对象的时候进行,此时检查是否满足垃圾回收条件,如果满足则进行垃圾回收。

自动垃圾回收相关函数malloc()gcShouldStart()gcinit()gcMark()

Go也可以通过 runtime.GC()手动阻塞触发GC。gcmark 在每次标记结束后会重置阈值大小。如果当前使用了 4MB 内 存,这时设置 gc_trigger 为 2 × 4MB,也就是当内存分配到 8MB 时会再次触发 GC; 回收之后内存为 5MB ,那下一次就要达到 10MB 才会触发 GC 。 这个比例 C triggerRatio )是由gcpercent/ 100 决定的。

gcpercent 的值是通过环境变量 GOGC 获取的,如果不设置这个环境变量,默认值是100。 如果将它设置成 off,则关闭垃圾回收。

如果系统启动或短时间内大量分配对象,会将垃圾回收的 gc_trigger 推高。 在服务正常后,活跃对象远小于这个阈值,造成垃圾回收无法触发,这个问题交给 sysmon 解决,它每隔 2 分钟强制触发 GC 一次。 这个 forcegc 的 goroutine 一直驻留在后台,直到 sysmon 它唤醒开始执行 GC 而不用检查阔值。

三 标记实现

GC 开始之前,有一些准备工作,整个 GC 启动过程都是 STW 状态,它启动了所有将并发执行标记工作的 goroutine,然后进入 GCMark 状态启动写屏障,启动 gcController,对应函数是 gcStart()

gcstart 会为所有的 P 都准备好对应的 goroutine worker,但是这些 worker 需要被 gcController 的 findRunnableGCWorker 唤醒才能工作。 M 启动后会一直通过 schedule 查找可执行的 G,其中 gcworker 也是 G 的一部分,但是首先要检查当前状态是不是回收状态,如果是才唤醒 worker 开始标记工作。

标记阶段是并行的,通过在后台一直运行标记worker老实现,对应源码函数是 gcBgMarkStartWorkers()

结束后调用 gcMarkDone , 这里会引起 StopTheWorld,接下来进入 gcMarkTermination 中的 gcMark 阶段 。

四 清理

GC标记结束后会触发清理 gcSweep,如果是并发清除,需要回收从 gc_trigger 到当前活跃内存大小相同的 heap 区域, 唤醒后台的 sweep goroutine。

对于并行式清理,在 GC 初始化的时候就会启动 bgsweep(),然后在后台一直循环,它会执行 gosweepone。 sweepone ( 一个内置的检查方法)首先会遍历所有的 spans 看它的 sweepgen 是否需要检查,如果要就检查这个 MSpan 里所有的 object 的 bit (位),看是否需要回收。这个过程可能触发 MSpan 到 MCentral 的回收,最终可能回收到 MHeap 的空闲列表当中。在空闲列表当中的内存在超过一定阁值时间后会被 sysmon 建议交还给内核。

五 监控

Go的GC除了可以自动检测、用户主动调用触发外,Go本身还会对运行状态进行监控,如果超过两分钟没有GC,则触发GC。监控函数是sysmon(),在主 goroutine 中启动。该goroutine不管有没有P,都会一直运行,所以也不允许写障碍。