前言

因为之前作为兴趣粗略的研究过Java的各种垃圾回收(CMS、G1、ZGC等),今天来大概了解一下Go的GC,如有错误,欢迎斧正。

一、先来了解一下常见GC算法

常见的 GC 算法。引用计数法、复制算法、标记-清除法、标记整理法、三色标记法、分代收集法。

1. 引用计数法

原理是在每个对象内部维护一个整数值,叫做这个对象的引用计数;

当对象被引用时引用计数加一,当对象不被引用时引用计数减一。当引用计数为 0

时,自动销毁对象。

简单但是速度很慢,缺陷是不能处理循环引用的情况。

2. 复制算法、标记-清除法、标记整理法

这哥仨是JVM的,今天不是主角,就简单说说

复制算法:一块空间均等割裂成两块,存活的复制过去,另一块清理;其实了解Java的都知道年轻代其实不是均等隔成两块,是8:1:1划分。好处是实现简单,运行高效,缺点也看到了,内存缩小为原来的一半。

标记-清除法:首先标记出所有需要回收的对象(Java是通过GCRoot),在标记完成后统一回收所有被标记的对象。缺点:内存碎片;

标记整理法:标记过程仍然与”标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存。解决内存碎片,代价是时间。

3. 分代收集

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

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

原理如下:

新对象放入第 0 代

当内存用量超过一个较小的阈值时,触发 0 代收集

第 0 代幸存的对象(未被收集)放入第 1 代

只有当内存用量超过一个较高的阈值时,才会触发 1 代收集

2 代同理

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

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

4. 三色标记法

三色标记法是传统 Mark-Sweep 的一个改进,是一个并发的 GC 算法。

首先做个规定:

初始所有对象都是白色(未遍历到)

从根出发能可达的对象是灰色

子节点遍历完的对象是黑色

有了这个,我们来看算法:

初始所有对象都是白色。

从根出发扫描所有可达对象,标记为灰色,放入待处理队列。

从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。

重复 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。

像CMS和G1都是用到了这个,只是做了亿点点改进,而且三色标记会产生浮动垃圾,这是不可避免的,而且CMS就是在三色标记这块有个大bug导致STW有时候巨长,所以JDK没用他做默认垃圾回收器,但是CMS仍然是一个承上启下的开创性的并发垃圾回收器。

二、现在来看Go GC

1. GC流程

GO的GC是并行GC, 也就是GC的大部分处理和普通的go代码是同时运行的, 这让GO的GC流程比较复杂.

Stack scan:Collect pointers from globals and goroutine stacks。收集根对象(全局变量,和G stack),开启写屏障。全局变量、开启写屏障需要STW。

Mark: Mark objects and follow pointers。标记所有根对象,和根对象可以到达的所有对象不被回收。

Mark Termination: Rescan globals/changed stack, finish mark。重新扫描全局变量,和上一轮改变的stack(写屏障),完成标记工作。这个过程需要STW。

Sweep: 按标记结果清扫span

目前整个GC流程会进行两次STW(Stop The World), 第一次是Stack scan阶段, 第二次是Mark Termination阶段.

第一次STW会准备根对象的扫描, 启动写屏障(Write Barrier)和辅助GC(mutator assist).

第二次STW会重新扫描部分根对象, 禁用写屏障(Write Barrier)和辅助GC(mutator assist).

从1.8以后的golang将第一步的stop the world 也取消了,这又是一次优化;

1.9开始, 写屏障的实现使用了Hybrid Write Barrier, 大幅减少了第二次STW的时间.

2. GC触发条件

主动的话,通过调用 runtime.GC(),这是阻塞式的。

自动垃圾回收的触发条件有两个:

超过内存大小阈值

达到定时时间

阈值是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发。

比如一次回收完毕后,内存的使用量为5M,那么下次回收的时机则是内存分配达到10M的时候。也就是说,并不是内存分配越多,垃圾回收频率越高。

如果一直达不到内存大小的阈值呢?这个时候GC就会被定时时间触发,比如一直达不到10M,那就定时(默认2min触发一次)触发一次GC保证资源的回收。