GO GC 垃圾回收机制

垃圾回收(Garbage Collection,简称GC)是编程语言中提供的内存管理功能。javascript

在传统的系统级编程语言(主要指C/C++)中,程序员定义了一个变量,就是在内存中开辟了一段相应的空间来存值。因为内存是有限的,因此当程序再也不须要使用某个变量的时候,就须要销毁该对象并释放其所占用的内存资源,好从新利用这段空间。在C/C++中,释放无用变量内存空间的事情须要由程序员本身来处理。就是说当程序员认为变量没用了,就手动地释放其占用的内存。可是这样显然很是繁琐,若是有所遗漏,就可能形成资源浪费甚至内存泄露。当软件系统比较复杂,变量多的时候程序员每每就忘记释放内存或者在不应释放的时候释放内存了。这对于程序开发人员是一个比较头痛的问题。php

为了解决这个问题,后来开发出来的几乎全部新语言(java,python,php等等)都引入了语言层面的自动内存管理 – 也就是语言的使用者只用关注内存的申请而没必要关心内存的释放,内存释放由虚拟机(virtual machine)或运行时(runtime)来自动进行管理。而这种对再也不使用的内存资源进行自动回收的功能就被称为垃圾回收。java

垃圾回收常见的方法

引用计数(reference counting)python

引用计数经过在对象上增长本身被引用的次数,被其余对象引用时加1,引用本身的对象被回收时减1,引用数为0的对象即为能够被回收的对象。这种算法在内存比较紧张和实时性比较高的系统中使用的比较普遍,如ios cocoa框架,php,python等。ios

优势:git

一、方式简单,回收速度快。程序员

缺点:github

一、须要额外的空间存放计数。golang

二、没法处理循环引用(如a.b=b;b.a=a这种状况)。算法

三、频繁更新引用计数下降了性能。

标记-清除(mark and sweep)

该方法分为两步,标记从根变量开始迭代得遍历全部被引用的对象,对可以经过应用遍历访问到的对象都进行标记为“被引用”;标记完成后进行清除操做,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操做)。这种方法解决了引用计数的不足,可是也有比较明显的问题:每次启动垃圾回收都会暂停当前全部的正常代码执行,回收是系统响应能力大大下降!固然后续也出现了不少mark&sweep算法的变种(如三色标记法)优化了这个问题。

复制收集

复制收集的方式只须要对对象进行一次扫描。准备一个「新的空间」,从根开始,对对象进行扫,若是存在对这个对象的引用,就把它复制到「新空间中」。一次扫描结束以后,全部存在于「新空间」的对象就是全部的非垃圾对象。

这两种方式各有千秋,标记清除的方式节省内存可是两次扫描须要更多的时间,对于垃圾比例较小的状况占优点。复制收集更快速可是须要额外开辟一块用来复制的内存,对垃圾比例较大的状况占优点。特别的,复制收集有「局部性」的优势。

在复制收集的过程当中,会按照对象被引用的顺序将对象复制到新空间中。因而,关系较近的对象被放在距离较近的内存空间的可能性会提升,这叫作局部性。局部性高的状况下,内存缓存会更有效地运做,程序的性能会提升。

对于标记清除,有一种标记-压缩算法的衍生算法:

对于压缩阶段,它的工做就是移动全部的可达对象到堆内存的同一个区域中,使他们紧凑的排列在一块儿,从而将全部非可达对象释放出来的空闲内存都集中在一块儿,经过这样的方式来达到减小内存碎片的目的。

分代收集(generation)

这种收集方式用了程序的一种特性:大部分对象会从产生开始在很短的时间内变成垃圾,而存在的很长时间的对象每每都有较长的生命周期。

根据对象的存活周期不一样将内存划分为新生代和老年代,存活周期短的为新生代,存活周期长的为老年代。这样就能够根据每块内存的特色采用最适当的收集算法。

新建立的对象存放在称为 新生代(young generation)中(通常来讲,新生代的大小会比 老年代小不少)。高频对新生成的对象进行回收,称为「小回收」,低频对全部对象回收,称为「大回收」。每一次「小回收」事后,就把存活下来的对象归为老年代,「小回收」的时候,遇到老年代直接跳过。大多数分代回收算法都采用的「复制收集」方法,由于小回收中垃圾的比例较大。

这种方式存在一个问题:若是在某个新生代的对象中,存在「老生代」的对象对它的引用,它就不是垃圾了,那么怎么制止「小回收」对其回收呢?这里用到了一中叫作写屏障的方式。

程序对全部涉及修改对象内容的地方进行保护,被称为「写屏障」(Write Barrier)。写屏障不只用于分代收集,也用于其余GC算法中。

在此算法的表现是,用一个记录集来记录重新生代到老生代的引用。若是有两个对象A和B,当对A的对象内容进行修改并加入B的引用时,若是①A是「老生代」②B是「新生代」。则将这个引用加入到记录集中。「小回收」的时候,由于记录集中有对B的引用,因此B再也不是垃圾。

三色标记算法

三色标记算法是对标记阶段的改进,原理以下:

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

可视化以下。

三色标记的一个明显好处是可以让用户程序和 mark 并发的进行,具体能够参考论文:《On-the-fly garbage collection: an exercise in cooperation.》。Golang 的 GC 实现也是基于这篇论文,后面再具体说明。

GO的垃圾回收器

go语言垃圾回收整体采用的是经典的mark and sweep算法。

  • v1.3之前版本 STW(Stop The World)

    golang的垃圾回收算法都很是简陋,而后其性能也广被诟病:go runtime在必定条件下(内存超过阈值或按期如2min),暂停全部任务的执行,进行mark&sweep操做,操做完成后启动全部任务的执行。在内存使用较多的场景下,go程序在进行垃圾回收时会发生很是明显的卡顿现象(Stop The World)。在对响应速度要求较高的后台服务进程中,这种延迟简直是不能忍受的!这个时期国内外不少在生产环境实践go语言的团队都或多或少踩过gc的坑。当时解决这个问题比较经常使用的方法是尽快控制自动分配内存的内存数量以减小gc负荷,同时采用手动管理内存的方法处理须要大量及高频分配内存的场景。

  • v1.3 Mark STW, Sweep 并行

    1.3版本中,go runtime分离了mark和sweep操做,和之前同样,也是先暂停全部任务执行并启动mark,mark完成后立刻就从新启动被暂停的任务了,而是让sweep任务和普通协程任务同样并行的和其余任务一块儿执行。若是运行在多核处理器上,go会试图将gc任务放到单独的核心上运行而尽可能不影响业务代码的执行。go team本身的说法是减小了50%-70%的暂停时间。

  • v1.5 三色标记法

    go 1.5正在实现的垃圾回收器是“非分代的、非移动的、并发的、三色的标记清除垃圾收集器”。引入了上文介绍的三色标记法,这种方法的mark操做是能够渐进执行的而不需每次都扫描整个内存空间,能够减小stop the world的时间。 由此能够看到,一路走来直到1.5版本,go的垃圾回收性能也是一直在提高,可是相对成熟的垃圾回收系统(如java jvm和javascript v8),go须要优化的路径还很长(可是相信将来必定是美好的~)。

  • v1.8 混合写屏障(hybrid write barrier)

    这个版本的 GC 代码相比以前改动仍是挺大的,采用一种混合的 write barrier 方式 (Yuasa-style deletion write barrier [Yuasa ‘90] 和 Dijkstra-style insertion write barrier [Dijkstra ‘78])来避免 堆栈从新扫描。

    混合屏障的优点在于它容许堆栈扫描永久地使堆栈变黑(没有STW而且没有写入堆栈的障碍),这彻底消除了堆栈从新扫描的须要,从而消除了对堆栈屏障的需求。从新扫描列表。特别是堆栈障碍在整个运行时引入了显着的复杂性,而且干扰了来自外部工具(如GDB和基于内核的分析器)的堆栈遍历。

    此外,与Dijkstra风格的写屏障同样,混合屏障不须要读屏障,所以指针读取是常规的内存读取; 它确保了进步,由于物体单调地从白色到灰色再到黑色。

    混合屏障的缺点很小。它可能会致使更多的浮动垃圾,由于它会在标记阶段的任什么时候刻保留从根(堆栈除外)可到达的全部内容。然而,在实践中,当前的Dijkstra障碍可能几乎保留不变。混合屏障还禁止某些优化:特别是,若是Go编译器能够静态地显示指针是nil,则Go编译器当前省略写屏障,可是在这种状况下混合屏障须要写屏障。这可能会略微增长二进制大小。

小结:

经过go team多年对gc的不断改进和忧化,GC的卡顿问题在1.8 版本基本上能够作到 1 毫秒如下的 GC 级别。 实际上,gc低延迟是有代价的,其中最大的是吞吐量的降低。因为须要实现并行处理,线程间同步和多余的数据生成复制都会占用实际逻辑业务代码运行的时间。GHC的全局中止GC对于实现高吞吐量来讲是十分合适的,而Go则更擅长与低延迟。
并行GC的第二个代价是不可预测的堆空间扩大。程序在GC的运行期间仍能不断分配任意大小的堆空间,所以咱们须要在到达最大的堆空间以前实行一次GC,可是过早实行GC会形成没必要要的GC扫描,这也是须要衡量利弊的。所以在使用Go时,须要自行保证程序有足够的内存空间。

垃圾收集是一个难题,没有所谓十全十美的方案,一般是为了适应应用场景作出的一种取舍。

相信GO将来会更好。

参考:

https://github.com/golang/pro...

http://legendtkl.com/2017/04/...

https://blog.twitch.tv/gos-ma...

https://blog.plan99.net/moder...

links

相关文章
相关标签/搜索