Golang_GC笔记

451 阅读7分钟

Go的GC算法

我们先来看下Golang中的GC是怎么实现的:

作为一门自带垃圾回收机制的语言,Go中的GC采用了并发三色标记+混合写屏障机制实现了垃圾回收算法。

并发三色标记

Golang GC中用到的三色标记算法属于标记-清除算法的一种,由荷兰科学家Dijikstra提出,主要有以下几个要点:

  • 所有对象分为三种颜色:黑色、灰色、白色;

    • 黑色对象表示,对象自身存活,并且其指向对象都已经标记完成;
    • 灰色对象表示:对象自身存活,但是其指向对象还未标记完成;
    • 白色对象表示:对象尚未被标记到,可能是垃圾对象;
  • 标记开始前,将根对象(全局对象、栈上的局部变量等)标记为黑色对象,将其所指向的对象标记为灰色对象

  • 标记规则是:从灰色对象出发,将其所指向的对象全部标记为灰色对象;该对象指向的所有对象标记为灰色对象以后,将当前的灰色对象标记为黑色对象;

  • 标记结束以后,白色对象就是不可达的垃圾对象,需要进行清扫;

image.png

注意:在go1.5版本以后,引入了并发垃圾回收机制,允许用户协程和后台的GC协程并发运行,这意味着在进行标记时,用户协程可能会对对象之间的引用关系进行调整,这会严重打乱GC三色标记时的标记秩序,可能会引发漏标和超标的问题

漏标问题

漏标问题指的是:在用户协程和后台GC协程并发执行的场景下,部分存活对象未被标记但是被误删的情况,这一问题产生过程如下:

  • 条件:初始时刻,对象B持有对象C的引用
  • 时刻1:GC协程下,对象A被扫描完成,标记为黑色,此时对象B是灰色,还未完成扫描;
  • 时刻2:用户协程下,对象A建立对对象C的引用;
  • 时刻3:用户协程下,对象B删除对对象C的引用;
  • 时刻4:GC协程下,开始执行对对象B的扫描;

在上述场景中,由于GC协程在B删除对C的引用之后才开始扫描B,因此无法到达C,又因为A已经被标记为黑色,不会重复扫描,所以C会被当做垃圾回收掉。

但是事实上,C由于被用户协程下的A引用,应该是存活的,这就因为漏标问题导致对象被误删的情况。这种问题是无法被接受的,其导致的误删现象可能会导致程序出现致命错误。

image.png

超标问题

超标问题指的是:在用户协程和后台GC协程并发执行的场景下,部分垃圾对象被误标记导致GC未按时将其回收的问题,这一问题产生过程如下:

  • 条件:初始时刻,对象A持有对象B的引用
  • 时刻1:GC协程下,对象A被扫描完成,标记为黑色,对象B被对象A引用,标记为灰色;
  • 时刻2:用户协程下,对象A删除对象B的引用;

上述场景中,事实上B被A删除引用以后,已经成为了垃圾对象,但是由于已经被标记为灰色,所以最终会被更新为黑色,不会被GC删除。

超标问题相比于漏标问题是相对可以接受的,其导致GC的回收精度下降,本应该被删除的对象可能会存活至下一轮GC,这部分对象才会被正确回收。

image.png

混合写屏障技术

为了解决并发标记算法中存在的漏标和超标问题,Go引入了写屏障技术。

漏标问题的本质是:一个已经扫描完成的黑色对象指向了一个被灰色/白色对象删除引用的白色对象。

一套用于解决漏标问题的方法论称为强弱三色不变式:

  • 强三色不变式:白色对象不能被黑色对象直接引用;
  • 弱三色不变式:白色对象可以被黑色对象引用,但是要保证从某个灰色对象出发仍然可达该白色对象;

屏障机制类似于一个回调保护机制,指的是在完成某个特定的动作之前,会先完成屏障成设置的内容。

插入写屏障的目标是实现强三色不变式,保证一个黑色对象指向一个白色对象之前,会先触发屏障将白色对象标记为灰色对象,然后再建立引用。

image.png

删除写屏障的目标是实现弱三色不变式,保证当一个白色对象即将被上游删除引用前,会触发屏障将其标记为灰色对象,之后再删除上游对其的引用。

image.png

虽然插入写屏障和删除写屏障两者择其一,即可解决GC的漏标问题。至于超标问题,可以采用容忍态度,放到下一轮GC中延后进行处理。

然而由于开启屏障技术需要进行STW(Stop The World)操作,导致应用程序暂停,会对服务性能造成很大影响,因此屏障机制无法应用于栈对象

为了解决这个问题,Go在1.8版本引入了混合写屏障机制,集插入删除写屏障之长,要点如下:

  • GC开始之前,以栈为单位分批扫描,将栈中所有对象标记为黑色对象;
  • GC期间,栈上新创建的对象直接标记为黑色;
  • 堆上的对象正常启用插入写屏障和删除写屏障;

混合写屏障,就不需要STW了吗?

虽然有了混合写屏障技术,但是Go语言的整个GC过程还是有两次STW,因为写屏障需要开启和关闭,在整个标记过程开始之前需要STW,用于开启写屏障,为标记做准备;在标记终止阶段同样需要短暂的STW来关闭写屏障。

阶段描述赋值器状态
SweepTermination清除终止阶段,为下一个阶段的并发标记做准备工作,启动写屏障STW
Mark扫描标记阶段,与赋值器并发执行,写屏障开启并发
MarkTermination标记终止阶段,保证一个周期内标记任务完成,停止写屏障STW
GCoff内存清除阶段,将需要回收的内存归还到堆中,写屏障关闭状态并发
GCoff内存归还阶段,将需要回收的内存归还到操作系统中,写屏障关闭状态并发

Go GC的触发时机

要尝试对GC进行优化,首先我们要知道GC会在哪些条件下被触发:

定时触发

Go会在程序启动的时候开启一个协程,异步每两分钟检查一次,发现距离上一次触发GC超过两分钟的时候,会强制触发一次GC;

对象分配触发

在分配对象的malloc方法中,如果满足以下两个条件之一,都会调用gcTrigger.test方法,发起一次触发GC的尝试:

  • 需要初始化一个大小超过32KB的大对象;
  • 待初始化对象在mcache中对应spanClass的mspan空间已经用完;

在gcTrigger.test方法中,针对gcTriggerHeap类型的触发时间,其校验条件是判断当前堆已使用的内存是否已经达到阈值。此处的堆内存阈值会在上一轮GC结束时进行设定。可以通过环境变量GOGC进行控制。

手动触发

通过调用runtime.GC()这个方法,可以阻塞式地等待当前GC运行完毕。