前言
因为之前作为兴趣粗略的研究过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保证资源的回收。