GC垃圾回收
Gc是语言提供的自动的内存管理机制,自动释放不需要的内存对象,让出存储器资源。
Go语言变革:
V1.5的三色并发标记法
V1.5的三色并发标记为什么需要STW
V1.5的三色标记为什么需要屏障机制(“强-弱”,三色不变式、插入屏障、删除屏障)
V1.8混合写屏障机制
V1.8混合写屏障机制的全场景分析
GoV1.3之前的标记-清除算法
- 标记
- 删除
标记清除算法的具体步骤
-
暂停程序业务逻辑,分类出可达和不可达对象,然后做上标记
-
开始标记,程序找出它所有可达的对象,并做上标记。
-
标记完之后,清除未被标记的对象
在mark和sweep算法在执行的时候,需要程序暂停! STW过程中,CPU不执行用户代码全部用于垃圾回收,影响很大急需优化。因此在执行第三步时,程序会暂停所有工作,一直等待所有回收执行完成。
- 停止暂停,继续执行程序,然后循环重复这个过程,直到process程序生命周期结束。
标记-清除的缺点
- STW让程序暂停,程序出现卡顿
- 标记需要扫描整个heap(堆);
- 清除数据会产生heap(堆)碎片;
V1.3版本之前
V1.3版本优化
上图主要是将STW步骤提前了一步,因为在sweep清除的时候,可以不需要STW停止,这些对象不会出现回收写冲突问题。
但是这个最大的问题是–STW会暂停整个程序。
因此出现三色并发标记法优化。
V1.5三色并发标记法
Golang中GC可以和其他用户goroutine并发运行但是需要一定时间STW,三色标记法就是通过三个阶段的标记来确定清除的对象都有哪些
三色并发标记法步骤
- 每次新创建的对象,默认的颜色都是标记为“白色”
上图所示,我们的程序可抵达的内存对象关系如左图所示,右边的标记表,是用来记录目前每个对象的标记颜色分类。程序其实是一些对象的根节点集合。将程序展开,会得到类似如下的表现形式。
- 每次GC回收开始,会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入灰色集合
这里的遍历是一次遍历,非递归形式,是从程序抽次可抵达的对象遍历一层,当前可抵达的对象是对象1和对象4,本次遍历结束,1和4就会被标记为灰色,灰色标记表就会多出两个对象。
3. 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
这一次遍历只是扫描灰色对象,将灰色对象的第一层可遍历可抵达的对象有白色变为灰色如对象2 和对象7。而之前的灰色对象1和对象4会被标记为黑色,由灰色标记表移动到黑色标记表中。
4. 重复第三步,知道灰色中无任何对象
当我们全部的可达对象都遍历完后,灰色标记表将不再存在灰色对象,目前全部内存的数据只有两个颜色,黑色和白色。黑色对象是我们程序逻辑可达对象,是合法有用的数据不可删除,白色的对象全部不可达对象,目前程序不依赖即垃圾数据
5. 回收所有的白色标记表的对象,也就是回收垃圾
以上就是三色并发标记法,但是这里面可能会有很多并发流程均会被扫描,执行并发流程的内存可能相互依赖,为了在GC过程中保证数据的安全,我们在开始三色标记之前就会加上STW,在扫描确定黑白对象之后再放开STW。但是这样明显GC扫描的性能实在是太低了。
没有STW的三色标记法
我们把初始状态设置为已经经历了第一轮扫描,目前黑色的有对象1和对象4,灰色的有对象2和对象7,其它均为白色;其中对象2通过指针p指向对象3
如果三色标记过程不启动STW,那么在GC扫描过程中,任意的对象均可能发生读写。如图,在扫描对象2之前,已经标记为黑色的对象4,此时创建指针q,并且指向白色的对象3
同时灰色对象2将指针p移除,那么白色的对象实则就是被挂在了已经扫描完成的黑色的对象4下
然后我们将所有灰色对象标记为黑色,那么对象2 和对象7就被标记成了黑色
然后将白色对象当做垃圾进行回收
这就出现了对象4合法引用的对象3被GC误杀回收掉了
屏障机制
三色标记不希望发生的情况
没有STW会出现的情况
条件1.一个白色对象被黑色对象引用
条件2.灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色)
满足以上两种条件就会出现对象丢失现象。
(1). “强-弱”三色不变式
- 强三色不变式–不允许黑色对象引用白色对象
- 弱三色不变式
黑色对象只能引用存在被其他灰色对象引用或者可达它的链路上游有灰色对象的白色对象
GC遵循上述两种方式,演变出两种屏障方式
插入屏障
在A引用对象B时,B对象内标记为灰色。满足强三色不变式
写屏障操作在栈空间对象操作中不使用,因为栈空间调用更加频繁要求速度快,如果引入写屏障机制会增加额外的开销。
因此插入屏障仅仅用于堆空间对象的操作中。
如果栈不添加插入屏障,三色标记后可能存在白色对象被引用的情况(GC不清理栈空间),(这个栈上的白色对象可能会逃逸到堆上,导致被清除) 所以要对栈重新进行三色标记扫描,此时需要启用STW直到栈空间的三色标记结束。
清除所有白色节点
删除屏障
具体操作
:被删除的对象(即不再被其他对象引用),如果自身为灰色或者白色,那么被标记为灰色。为了确保其可达性在后续标记阶段被重新检查,避免因并发操作导致的错误回收。
满足
:弱三色不变式(保证灰色对象到白色对象的路径不会断)
这种方式回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清除。
插入屏障和删除屏障的短板:
- 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象。
- 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻所有存活对象。
V1.8混合写屏障机制
混合写屏障规则
具体操作
:
- GC开始直接将栈上的对象标记为黑色(只扫描这一次
- GC期间,任何在栈上创建的新对象,均为黑色(防止内存逃逸,栈上的白色对象逃逸到堆上被GC回收)
- 被删除的对象标记为灰色
- 被添加的对象标记为灰色
满足
:变形的弱三色不变式
屏障技术不在栈上应用,保证栈的运行效率
混合写屏障的具体场景分析
注意混合写屏障是Gc的一种屏障机制,所以只是当程序执行GC的时候,才会触发这种机制。
GC开始:扫描栈区,将可达对象全部标记为黑色
场景一:对象被一个堆对象删除引用,成为栈对象的下游
场景二:对象被一个栈对象删除引用,成为另一个栈对象的下游
场景三:对象被一个堆对象删除引用,成为另一个堆对象的下游
场景四:对象从一个栈对象删除引用,成为另一个堆对象的下游
Golang中的混合写屏障满足弱三色不变式
,结合了删除写屏障和插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。
辅助GC(Mutator Assist)
为了防止内存分配过快,在GC执行过程中,如果goroutine需要分配内存,那么这个goroutine会参与一部分GC的工作,即帮助GC做一部分工作,这个机制叫作Mutator Assist。
垃圾回收触发时机
内存分配量达到阈值触发GC
默认情况下,最长2分钟触发一次GC,这个间隔在src/runtime/proc.go:forcegcperiod
变量中被声明:
// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
//
// This is a variable for testing purposes. It normally doesn't change.
var forcegcperiod int64 = 2 * 60 * 1e9
手动触发
程序代码中也可以使用runtime.GC()
来手动触发GC。这主要用于GC性能测试和统计。
GC性能优化
GC性能与对象数量负相关,对象越多GC性能越差,对程序影响越大。
所以GC性能优化的思路之一就是减少对象分配个数,比如对象复用或使用大对象组合多个小对象等等。
另外,由于内存逃逸现象,有些隐式的内存分配也会产生,也有可能成为GC的负担。