垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的内存对象,让出存储器资源。GC过程中无需程序员手动执行。GC机制在现代很多编程语言都支持,GC能力的性能与优劣也是不同语言之间对比度指标之一。

Golang在GC的演进过程中也经历了很多次变革

  • Go V1.3之前的标记-清除(mark and sweep)算法
  • Go V1.5的三色并发标记法
  • Go V1.8混合写屏障机制

需要关注业务,而不是关于语言本身内存使用的信息

STW(stop the world)——性能瓶颈

标记-清除算法

  • 算法步骤:

    1. STW,划分出可达对象和不可达对象
    2. 标记出所有的可达对象
    3. 清除未标记的对象
    4. 停止暂停,继续运行程序,循环上述过程直至程序生命周期结束
  • 算法缺点:

    STW

    标记需要扫描整个heap

    清除数据会产生heap碎片

  • STW时间较长:

    解决上述问题的一个小优化是等STW结束后再去Sweep清除未标记的对象

    由于所有未标记的对象都是不可达的,所以不用担心回收写冲突等问题

    但是无论怎么优化,Go V1.3都面临这个一个重要问题,就是mark-and-sweep 算法会暂停整个程序

    V1.5版本 用三色并发标记法来优化这个问题

三色并发标记法

三色是黑白灰,顺序是白→灰→黑

可以看出,有两种情况,在三色标记法中,是不希望被发生的。

  • 条件1: 一个白色对象被黑色对象引用**(白色被挂在黑色下)**
  • 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏**(灰色同时丢了该白色)**

如果当以上两个条件同时满足时,就会出现对象丢失现象!

两种屏障:

  • 插入写屏障:(仅堆中可执行)

    规则:当一个对象引用另外一个对象时,将另外一个对象标记为灰色。

    满足:强三色不变式。不会存在黑色对象引用白色对象

    这里需要注意一点,插入屏障仅会在堆内存中生效,不对栈内存空间生效,这是因为go在并发运行时,大部分的操作都发生在栈上,函数调用会非常频繁。数十万goroutine的栈都进行屏障保护自然会有性能问题。

    缺点:所以只使用插入写屏障的话,可能发生栈上对象新增被引用,但无法执行插入写屏障,而被错误回收。所以插入写屏障最大的弊端就是,在一次正常的三色标记流程结束后,需要对栈上重新进行一次STW,然后再rescan一次。

  • 删除写屏障:(堆栈中均可执行)

    规则:在删除引用时,如果被删除的引用关系的被引用对象为灰色或者白色,那么被标记为灰色。

    满足弱三色不变式。灰色对象到白色对象的路径不会断

    解释:白色对象始终会被灰色对象保护

    缺点:但是引入删除写屏障,有一个弊端,就是一个对象的引用被删除后,即使没有其他存活的对象引用它,它仍然会活到下一轮。如此一来,会产生很多的冗余扫描成本,且降低了回收精度
    另外,在GC开始时,STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象,从而在删除操作时,可以拦截操作,将白色对象置为灰色对象。

插入写屏障机制和删除写屏障机制中任一机制均可保护对象不被丢失。在V1.5的版本中采用的是插入写机制实现。

混合写屏障

规则:

GC刚开始的时候,会将栈上的可达对象全部标记为黑色。

GC期间,任何在栈上新创建的对象,均为黑色。

上面两点只有一个目的,将栈上的可达对象全部标黑,最后无需对栈进行STW,就可以保证栈上的对象不会丢失。有人说,一直是黑色的对象,那么不就永远清除不掉了么,这里强调一下,标记为黑色的是可达对象,不可达的对象一直会是白色,直到最后被回收。

堆上被删除的对象标记为灰色

堆上新添加的对象标记为灰色

另外,

一个对象之所以可以引用另外一个对象,它的前提是需要另外一个对象可达

即无法引用一个不可达的对象,会出现的只有:单纯的删除引用,以及

  1. 场景一: 对象被一个堆对象删除引用,成为栈对象的下游

    “被堆删除引用”触发删除写屏障,标记被删除引用的对象为灰色

  2. 场景二: 对象被一个栈对象删除引用,成为另一个栈对象的下游

    栈不启动屏障,但此时由于所有可达对象在GC开始时均已标记为黑色,所以无事发生

  3. 场景三:对象被一个堆对象删除引用,成为另一个堆对象的下游

    对同一个堆对象触发插入写屏障和删除写屏障,该对象被标记为灰色

  4. 场景四:对象从一个栈对象删除引用,成为另一个堆对象的下游

    栈不启动屏障,但此时由于所有可达对象在GC开始时均已标记为黑色,所以无事发生

总结

Golang v1.3之前采用传统采取标记-清除法,需要STW,暂停整个程序的运行。

在v1.5版本中,引入了三色标记法和插入写屏障机制,其中插入写屏障机制只在堆内存中生效。但在标记过程中,最后需要对栈进行STW。

在v1.8版本中结合删除写屏障机制,推出了混合屏障机制,屏障限制只在堆内存中生效。避免了最后节点对栈进行STW的问题,提升了GC效率。

参考

详细总结: Golang GC、三色标记、混合写屏障机制

Golang三色标记混合写屏障GC模式全分析