golang入门系列之Go GC

    科技2023-10-29  104

    前言

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


    Processed: 0.020, SQL: 8