GC 和 Golang 的垃圾回收机制

思维导图

GC的概念

GC全称是 garbage collection,即垃圾回收。

新开发的语言(java,python,php等等),在使用的时候,可以使语言用户不必关心内存对象的释放,只需要关心对象的申请即可

Golang本身就是一门基于 garbage collection 的语言,垃圾回收由语言本身机制实现。

gc与程序交互时候的效率会影响到整个程序的运行效率。通常程序本身的内存管理会影响gc和程序之间的效率,甚至造成性能瓶颈。

常见的GC模式

引用计数

  1. 流程
    • 每个对象内部维护一个整数值,叫做这个对象的 引用计数
    • 对象被引用的时候 +1,对象不被引用的时候 -1
    • 引用计数 归零的时候,对象会被销毁
  2. 缺陷
    • 循环引用问题:对象A和B互相引用,计数器都不为0,那么永远不会被收集
    • 引用计数的赋值会造成性能消耗
  3. Python、PHP、 c++ 标准库的 std::shared_ptr 、微软的 COM 、Objective-C

标记清除 Mark-Sweep

  1. 流程

    • 标记:从程序根结底开始,递归遍历所有对象,并打上标记,标记为存活
    • 清除:清除阶段,将所有未被标记的对象当作垃圾销毁
  2. 缺陷

    • 最大的缺陷就是所谓的STWstop the world)。在垃圾回收阶段,所有程序将被挂起暂停,等待恢复完毕后再执行

      就如同现在的疫情,湖北省各大城市按下了暂停键,要对病情进行控制,疑似和确诊病例进行清理。按照现在的执行效率,相信很快就能清理完(2020.3.17 周边城市已经解封,49支医疗队陆续撤离湖北)

    • 显而易见的,清除后产生了大量不连续的内存碎片,导致在程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。

  3. 何时进行STW?

    堆中的有效内存空间(available memory)被耗尽的时候,就会让整个程序stop the world, 然后进行两项工作,第一是标记,第二是清除

  4. 为何要STW?

    GC的过程中,其他线程的代码可能会改变对象状态,可能把不应该回收的对象当做垃圾收集掉

    eg: A刚刚被标记完,如果未暂停程序,申请了一个B对象,且A可达B。那么B是未被标记的,后面会被意外地清理掉,引起程序错误。

Golang1.5 之前所使用

2014/6 1.3版本并发清理的引入,go runtime 分离了 mark 和 sweep 的操作

和以前一样,也是先暂停所有任务执行并启动 mark( mark 这部分还是要把原程序停下来的),mark 完成后就马上就重新启动被暂停的任务了,并且让 sweep 任务和普通协程任务一样并行,和其他任务一起执行。

如果运行在多核处理器上,go 会试图将 gc 任务放到单独的核心上运行而尽量不影响业务代码的执行,go team 自己的说法是减少了 50%-70% 的暂停时间。

基本算法就是之前提到的清扫+回收,Golang gc 优化的核心就是尽量使得 STW 的时间越来越短。

三色标记

内存管理篇(三):Go垃圾回收之三色标记算法

图片来自GO夜读

相比传统的标记清扫算法,三色标记最大的好处是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个 GC。

三色标记法因为多了一个白色的状态来存放不确定的对象,所以可以异步地执行。当然异步执行的代价是可能会造成一些遗漏,因为那些早先被标记为黑色的对象可能目前已经是不可达的了。

标记清除 Mark-Sweep 的变种

  1. 分类

    • 黑色:根对象,或者该对象与它的子对象都被扫描过(对象被标记了,且它的所有field也被标记完了)。
    • 灰色:对象本身被扫描,但还没扫描完该对象中的子对象(它的field还没有被标记或标记完)。
    • 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,既垃圾对象(对象没有被标记到)。
  2. 流程

    1. 创建三个集合:白、灰、黑。将所有对象放入白色集合中。
    2. 从根节点开始遍历所有对象(注意这里并不递归遍历),把遍历到的对象从白色集合放入灰色集合。
    3. 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
    4. 重复 3 直到灰色中无任何对象
    5. 通过write-barrier检测对象有变化,重复以上操作
    6. 收集所有白色对象(垃圾)

这个算法可以实现 “on-the-fly”,也就是在程序执行的同时进行收集,并不需要暂停整个程序。

但是也会有一个缺陷,可能程序中的垃圾产生的速度会大于垃圾收集的速度,这样会导致程序中的垃圾越来越多无法被收集掉。

标记的root根对象包括:全局变量,各个G stack上的变量等

而使用三色标记,即使在标记过程中对象的引用关系发生了改变,例如分配内存并修改对象属性域的值,只要满足黑色对象不引用白色对象的约束条件,垃圾回收器就可以继续正常工作。

于是每次并不需要将回收过程全部执行完,只是处理一部分后停下来,后续会慢慢再次触发的回收过程,实现增量回收。相当于是把垃圾回收过程打散,减少停顿时间。

Go 1.5、Go 1.6

1.5 版本进行了较大改进,使用了三色标记算法。go 1.5 在源码中的解释是“非分代的、非移动的、并发的、三色的标记清除垃圾收集器”

  • 非分代:不像java那样分为年轻代和老年代,自然也没有minor和majo gc的区别
  • 非紧缩:在垃圾回收之后不会进行内存整理以清除内存碎片
  • 写屏障:在并发标记的过程中,如果应用程序修改了对象图,就可能出现标记遗漏的可能,写屏障是为了处理标记遗漏的问题。

go 除了标准的三色收集以外,还有一个辅助回收功能,防止垃圾产生过快收集不过来的情况。这部分代码在 runtime.gcAssistAlloc 中。

分代清除

分代收集也是传统 Mark-Sweep 的一个改进。这个算法是基于一个经验:绝大多数对象的生命周期都很短。所以按照对象的生命周期长短来进行分代。

一般 GC 都会分三代,在 java 中称之为新生代(Young Generation)、年老代(Tenured Generation)和永久代(Permanent Generation);在 .NET 中称之为第 0 代、第 1 代和第2代。

流程:

  1. 新对象放入第 0 代
  2. 当内存用量超过一个较小的阈值时,触发 0 代收集
  3. 第 0 代幸存的对象(未被收集)放入第 1 代
  4. 只有当内存用量超过一个较高的阈值时,才会触发 1 代收集,2 代同理

因为 0 代中的对象十分少,所以每次收集时遍历都会非常快(比 1 代收集快几个数量级)。只有内存消耗过于大的时候才会触发较慢的 1 代和 2 代收集。

因此,分代收集是目前比较好的垃圾回收方式。使用的语言(平台)有 大名鼎鼎的JVM、.NET 。

Golang 未使用分代

golang 并没有分代收集,对于巨量的小对象还是很苦手的,会导致整个 mark 过程十分长,在某些极端情况下,甚至会导致 GC 线程占据 50% 以上的 CPU。

当程序由于高并发等原因造成大量小对象的gc问题时,最好可以使用 sync.Pool 等对象池技术,避免大量小对象加大 GC 压力。

Golang 的 GC

版本 发布时间 GC算法 STW时间 重大更新
V1.1 2013/5 STW 可能秒级别
V1.3 2014/6 Mark和Sweep分离. Mark STW, Sweep并发 百ms级别
V1.4 2014/12 runtime代码基本都由C和少量汇编改为Go和少量汇编, 包括GC部分, 以此实现了准确式GC,减少了堆大小, 同时对指针的写入引入了write barrier, 为1.5铺垫 百ms级别
V1.5 2015/8 三色标记法, 并发Mark, 并发Sweep. 非分代, 非移动, 并发的收集器 10ms-40ms级别 重要更新版本,生产上GC基本不会成为问题
V1.6 2016/2 1.5中一些与并发GC不协调的地方更改. 集中式的GC协调协程, 改为状态机实现 5-20ms
V1.7 2016/8 GC时栈收缩改为并发, span中对象分配状态由freelist改为bitmap 1-3ms左右
V1.8 2017/2 hybird write barrier, 消除了stw中的重新扫描栈 sub ms Golang GC进入Sub ms时代

Golang进行GC的时机和流程

触发机制

  1. 在申请内存的时候,检查当前当前已分配的内存是否大于上次GC后的内存的2倍,若是则触发(主GC线程为当前M)
  2. 监控线程发现上次GC的时间已经超过两分钟了,触发;将一个G任务放到全局G队列中去。(主GC线程为执行这个G任务的M)

清理流程

  1. stop the world,等待所有的M休眠;此时所有的业务逻辑代码都停止
  2. 标记:分配gc标记任务,唤醒 gcproc个 M(就是第一步休眠的那些),分别做这个,直到所有的M都做完,才结束;并且所有M再次进入休眠
  3. 清理:有一个单独的goroutine去清理已经标记的内存对象快
  4. start the world,设置gcwaiting=0,唤醒所有的M(不会超过P个数)

Golang 内存内存分析调试方法


分享: