垃圾回收
GC相关术语
赋值器:说白了就是你写的程序代码,在程序的执行过程中,可能会改变对象的引用关系,或者创建新的引用。
回收器:垃圾回收器的责任就是去干掉那些程序中不再被引用得对象
STW:STW 可以是Stop The World的缩写,也可以是Start The World的缩写。通常意义上指的是从Stop The World到Start The World这一段时间间隔。垃圾回收过程中为了保证准确性、防止无止境的内存增长等问题而不可避免的需要停止赋值器进一步操作对象图以完成垃圾回收。STW时间越长,对用户代码造成的影响越大。
root对象:根对象是指赋值器不需要通过其他对象就可以直接访问到的对象,通过Root对象, 可以追踪到其他存活的对象。常见的root对象有:
- 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
- 执行栈:每个 goroutine (包括main函数)都拥有自己的执行栈,这些执行栈上包含栈上的变量及堆内存指针。【堆内存指针即在gorouine中申请或者引用了在堆内存的变量】
当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。
常规GC回收机制
- 引用计数(Reference Counting): 每个对象都有一个引用计数,当对象被引用时,计数加一,当引用失效时,计数减一。当计数为零时,表示对象不再被引用,可以被回收。这种方法简单,但难以处理循环引用的情况。
- 标记-清除(Mark and Sweep): 这是一种基于可达性的算法。首先,通过一个根集合(通常是全局变量和栈上的变量)标记所有可以从根集合访问到的对象。然后,清除所有未被标记的对象。这种方法能够处理循环引用,但会导致一些暂时的停顿,同时会造成不连续的内存空间(内存碎片),导致有大的对象创建的时候,明明内存中总内存是够的,但是空间不是连续的造成对象无法分配。
- 复制(Copying): 将内存分为两个区域,每次只使用其中一个。当这个区域满了,将还活跃的对象复制到另一区域,然后清除原区域中的所有对象。这种方法简化了垃圾回收的过程,但会浪费一半的内存空间,移动存活对象比较耗时,并且如果存活对象较多的时候,需要担保机制确保复制区有足够的空间可完成复制。
- 标记-整理(Mark and Compact): 类似于标记-清除,但在标记阶段之后,会将存活的对象整理到一侧,然后清除另一侧的所有对象,减少内存碎片。这种方法可以减少碎片,但也会导致一些停顿。
- 分代垃圾回收(Generational Garbage Collection): 将对象分为年轻代和老年代。新创建的对象首先分配到年轻代,而老年代存活时间较长。通过这种方式可以更有效地进行垃圾回收,因为大多数对象都具有较短的生命周期。
Golang垃圾回收算法
v1.3 标记清除法
- 开启STW,停止程序的运行。
- 从根节点出发,标记所有可达对象。
- 停止STW,然后回收所有未被标记的对象 。
v1.5 三色标记法
三色标记法将对象分为三类,并用不同的颜色相称:
- 白色:未搜索的对象,在回收周期开始时所有对象都是白色,在回收周期结束时所有的白色都是垃圾对象
- 灰色:正在搜索的对象,但是对象身上还有一个或多个引用没有扫描
- 黑色:已搜索完的对象,所有的引用已经被扫描完
标记过程如下:
- 初始时所有对象都是白色对象
- 从GC Root对象出发,扫描所有可达对象并标记为灰色,放入待处理队列
- 从队列取出一个灰色对象并标记为黑色,将其引用对象标记为灰色放入队列
- 重复上一步骤,直到灰色对象队列为空
- 此时所有剩下的白色对象就是垃圾对象
优点:
- 不需要暂停整个程序进行垃圾回收
缺点:
- 如果程序垃圾对象的产生速度大于垃圾对象的回收速度时,可能导致程序中的垃圾对象越来越多而无法及时收集
- 线程切换和上下文转换的消耗会使得垃圾回收的总体成本上升,从而降低系统吞吐量
垃圾回收的并发问题:
假设三色标记法和用户程序并发执行,那么下列两个条件同时满足就可能出现错误回收非垃圾对象的问题:
- 条件1:某一黑色对象引用白色对象
- 条件2:对于某个白色对象,所有和它存在可达关系的灰色对象丢失了访问它的可达路径
为了解决上述问题,就提出了强三色不变式和弱三色不变式。
强三色不变式
规则:不允许黑色对象引用白色对象
破坏了条件1: 白色对象被黑色对象引用
解释:如果一个黑色对象不直接引用白色对象,那么就不会出现白色对象扫描不到,从而被当做垃圾回收掉的尴尬。
弱三色不变式
规则:黑色对象可以引用白色对象,但是白色对象的上游必须存在灰色对象
破坏了条件2:灰色对象与白色对象之间的可达关系遭到破坏
解释: 如果一个白色对象的上游有灰色对象,则这个白色对象一定可以扫描到,从而不被回收
屏障机制
遵循上述两种不变式提到的原则,分别提出了两种实现机制:插入写屏障和删除写屏障。
插入写屏障:
规则:当一个对象引用另外一个对象时,将另外一个对象标记为灰色。
满足:强三色不变式。不会存在黑色对象引用白色对象
这里需要注意一点,插入屏障仅会在堆内存中生效,不对栈内存空间生效,这是因为go在并发运行时,大部分的操作都发生在栈上,函数调用会非常频繁。数十万goroutine的栈都进行屏障保护自然会有性能问题。
对于插入写屏障来讲,最大的弊端就是,在一次正常的三色标记流程结束后,需要对栈上重新进行一次stw,然后再rescan一次。
删除写屏障
规则:在删除引用时,如果被删除引用的对象自身为灰色或者白色,那么被标记为灰色。
满足:弱三色不变式。灰色对象到白色对象的路径不会断(白色对象始终会被灰色对象保护)
引入删除写屏障,有一个弊端,就是一个对象的引用被删除后,即使没有其他存活的对象引用它,它仍然会活到下一轮。如此一来,会产生很多的冗余扫描成本,且降低了回收精度,举例来讲。
从上面示例来看,插入写屏障机制和删除写屏障机制中任一机制均可保护对象不被丢失。在V1.5的版本中采用的是插入写机制实现。
对比插入写屏障和删除写屏障:
插入写屏障:
- 插入写屏障哪里都好,就是栈上的操作管不到,所以最后需要对栈空间进行stw保护,然后rescan保证引用的白色对象存活。
删除写屏障:
-
在GC开始时,会扫描记录整个栈做快照,从而在删除操作时,可以拦截操作,将白色对象置为灰色对象。
-
回收精度低。
v1.8 混合写屏障机制
在v1.8版本下引入了混合写屏障机制。下面我们看下混合屏障机制的核心定义:
- GC刚开始的时候,会将栈上的可达对象全部标记为黑色。
- GC期间,任何在栈上新创建的对象,均为黑色。
上面两点只有一个目的,将栈上的可达对象全部标黑,最后无需对栈进行STW,就可以保证栈上的对象不会丢失。有人说,一直是黑色的对象,那么不就永远清除不掉了么,这里强调一下,标记为黑色的是可达对象,不可达的对象一直会是白色,直到最后被回收。
- 堆上被删除的对象标记为灰色
- 堆上新添加的对象标记为灰色
总结
- Golang v1.3之前采用传统采取标记-清除法,需要STW,暂停整个程序的运行。
- 在v1.5版本中,引入了三色标记法和插入写屏障机制,其中插入写屏障机制只在堆内存中生效。但在最后需要对栈进行STW。
- 在v1.8版本中结合删除写屏障机制,推出了混合屏障机制,屏障限制只在堆内存中生效。避免了最后节点对栈进行STW的问题,提升了GC效率