结论:虽然Go如今的基于三色标识算法的垃圾回收已经被普遍承认,可是对比于JVM这种运行在虚拟机的GC(GO是在Runtime),仍是有明显的差距。   1.

Go 并发垃圾收集器

因为 Go 是一种命令式语言,它的值类型,内存访问模式和 C# (.NET 使用分代垃圾收集器)至关。php

事实上,Go 程序一般是处理 request/response 任务(如 HTTP 服务器),这意味着 Go 程序表现出强烈的代际行为,Go 团队正在探索潜在的能够利用代际假说的算法,他们称之为“面向请求的垃圾收集器”。这本质上是一个能够策略调优的分代垃圾收集器。在处理请求/响应这种模式时,经过确保年轻代足够大以使经过处理请求产生的全部垃圾都在其中来优化 GC。(高可用架构译者注:指的是 Go下一代垃圾收集器 Transaction-Oriented Collector)html

尽管如此,Go 的当前 GC 是不分代的。只是在后台运行标记/扫描。(高可用架构译者注:并发标记清除算法)
这样使暂停时间很是短 ,但使其余因素更糟糕。从咱们的基本理论上面咱们能够看到:java

  • GC吞吐量:GC时间与堆大小同步增加。简单来讲,你的程序使用的内存越多,内存释放速度就越慢,你的计算机花费的时间就越多。若是你的程序没有并行化,你能够不用考虑这个问题。
  • 整理:由于没有整理,GC 过程会产生内存碎片。程序也不会受益于在缓存中整齐排列的内容。
  • 程序吞吐量:由于GC必须在每一个周期作不少工做,因此会消耗很多CPU时间。
  • 暂停分布:与程序并发运行的任何垃圾收集器均可能遇到Java中“并发模式失败”的问题:您的程序建立垃圾的速度比GC线程能够清除它快。在这种状况下,runtime别无选择只能彻底中止程序,等待GC完成垃圾收集。所以当Go团队声明GC暂停很是低时,该声明只能适用于GC具备足够的CPU时间和空间以完成垃圾回收的状况。另外,因为Go编译器缺少确保线程能够被快速可靠暂停这一功能,会致使暂停时间是否很低取决于您运行的是什么类型的代码(例如,base64 解码单个 goroutine 中的大 blob 会致使暂停时间上升)。
  • 堆开销:由于经过标记/扫描收集堆很是慢,您须要大量的空间以确保不会碰见“并发模式故障”。 Go默认使用100%的堆开销会让程序须要的内存量增长一倍。

咱们能够看到这些权衡:python

服务1分配内存多于服务2,所以STW暂停在服务1中较高。但STW暂停持续时间在两个服务上都降低了一个数量级。咱们看到切换后,两个服务后在GC中花费的CPU使用率增长了约20%。ios

在这个特定的状况下,Go 以更慢的收集器为代价换取暂停时间的数量级降低。这是一个好的权衡吗?暂停时间已经足够低吗?c++

付出更多的硬件成本以得到较低的暂停时间,在一些状况下未必有意义。若是你的服务器暂停时间从 10msec 下降到 1msec,你的用户真的会注意到吗?若是你必须加倍你的机器数量才能达成这一目的呢?程序员

Go 将暂停时间优化做为首要目标,以致于它彷佛愿意将程序减慢至任何数量级,以得到较短暂停。算法

 

2.

 

一. 什么是垃圾回收

曾几什么时候,内存管理是程序员开发应用的一大难题。传统的系统级编程语言(主要指C/C++)中,程序员必须对内存当心的进行管理操做,控制内存的申请及释放。稍有不慎,就可能产生内存泄露问题,这种问题不易发现而且难以定位,一直成为困扰开发者的噩梦。如何解决这个头疼的问题呢?编程

过去通常采用两种办法:缓存

  1. 内存泄露检测工具。这种工具的原理通常是静态代码扫描,经过扫描程序检测可能出现内存泄露的代码段。然而检测工具不免有疏漏和不足,只能起到辅助做用。(Valgrind)
  2. 智能指针。这是c++中引入的自动内存管理方法,经过拥有自动内存管理功能的指针对象来引用对象,是程序员不用太关注内存的释放,而达到内存自动释放的目的。这种方法是采用最普遍的作法,可是对程序员有必定的学习成本(并不是语言层面的原生支持),并且一旦有忘记使用的场景依然没法避免内存泄露。

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

二. 常见的垃圾回收算法

① 引用计数

这是最简单的一种垃圾回收算法,和以前提到的智能指针殊途同归。对每一个对象维护一个引用计数,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被建立或被赋值给其余对象时引用计数自动加一。当引用计数为0时则当即回收对象。

这种方法的优势是实现简单,而且内存的回收很及时。这种算法在内存比较紧张和实时性比较高的系统中使用的比较普遍,如ios cocoa框架,php,python等。简单引用计数算法也有明显的缺点:

  1. 频繁更新引用计数下降了性能。一种简单的解决方法就是编译器将相邻的引用计数更新操做合并到一次更新;还有一种方法是针对频繁发生的临时变量引用不进行计数,而是在引用达到0时经过扫描堆栈确认是否还有临时对象引用而决定是否释放。等等还有不少其余方法,具体能够参考这里。
  2. 循环引用问题。当对象间发生循环引用时引用链中的对象都没法获得释放。最明显的解决办法是避免产生循环引用,如cocoa引入了strong指针和weak指针两种指针类型。或者系统检测循环引用并主动打破循环链。固然这也增长了垃圾回收的复杂度。

② 标记清除

该方法分为两步,标记从根变量开始迭代得遍历全部被引用的对象,对可以经过应用遍历访问到的对象都进行标记为“被引用”;标记完成后进行清除操做,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操做)。

这种方法解决了引用计数的不足,可是也有比较明显的问题:每次启动垃圾回收都会暂停当前全部的正常代码执行,回收是系统响应能力大大下降!固然后续也出现了不少mark&sweep算法的变种(如三色标记法)优化了这个问题。

③ 分代收集

通过大量实际观察得知,在面向对象编程语言中,绝大多数对象的生命周期都很是短。分代收集的基本思想是,将堆划分为两个或多个称为 代(generation)的空间。新建立的对象存放在称为 新生代(young generation)中(通常来讲,新生代的大小会比 老年代小不少),随着垃圾回收的重复执行,生命周期较长的对象会被 提高(promotion)到老年代中。所以,新生代垃圾回收和老年代垃圾回收两种不一样的垃圾回收方式应运而生,分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度很是快,比老年代快几个数量级,即便新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是由于大多数对象的生命周期都很短,根本无需提高到老年代。

三. Go的GC工做机制

Go的GC自打一开始就被不少人诟病,通过这么多年的发展Go的GC已经变得很是的优秀了,如下是Go的GC算法里程碑

  1. v1.1 STW (stop the world)
  2. v1.3 Mark STW, Sweep 并行
  3. v1.5 三色标记法
  4. v1.8 hybrid write barrier
Mark and Sweep (标记清除)

① Mark and Sweep

Mark and Sweep
  1. 标记(Mark): 找出全部不可达对象,而后作上标记
  2. 清除(Sweep): 回收标记好的对象

咱们经过如下图解来解释 标记清除 算法是如何工做的

  1. 开始标记,程序暂停,此时程序和对象的关系以下图所示
  2. 找到全部可达对象,并作上标记
  3. 标记完成后开始清除未标记的对象
  4. 清除完成后,对象以下图所示
标记清除
  1. STW (stop the world) 标记对象的时候程序须要暂停,致使程序出现卡顿 (最主要的问题)
  2. 标记须要扫描整个堆
  3. 清除对象会产生堆碎片
STW

② Tri-color Marking

标记清除Tri-color Marking
白色灰色灰色黑色灰色白色黑色白色
三色标记法STW
标记操做和用户逻辑并行写屏障清除操做和用户逻辑并行
经过容许用户逻辑在标记和清除操做上作到并行处理来缩短STW的时间,提高总体GC的性能。