over-golang/08-Go运行时/05-Go的GC机制.md
2021-07-02 18:11:59 +08:00

7.2 KiB
Raw Permalink Blame History

一 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之前垃圾回收的标记和清理都是STWStop 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都会一直运行所以也不允许写障碍。