常见的垃圾回收机制

引用计数

对每个对象维护一个引用计数,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被创建或被赋值给其他对象时引用计数自动加一。

缺点:

  • 降低性能
  • 循环引用

标记-清除

该方法分为两步:

  1. 标记从根节点开始迭代遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;
  2. 清除操作。对没有被标记的内存进行清除操作。

缺点:

  • 启动垃圾回收时会暂停当前所用代码的执行。

分代收集

分代收集的基本思想是,将堆划分成两个或者多个称为代的空间。新创建的对象存放在称为新生代中,随着垃圾回收的重复执行,生命周期较长的对象会被提升到老年代中,因此,新生代垃圾回收和老年代垃圾回收两种方式共存,分别对其空间中的对象进行垃圾回收。新生代垃圾回收的速度非常快,比老年代快几个数量级,即使新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是因为大多数对象的生命周期都很短,根本无需提升到老年代。

Go的GC

go的垃圾回收机制是标记-清除算法。

  • 标记阶段。获取这些对象的状态信息。
  • 清扫阶段。回收状态为unreachable的对象。

三色标记法

  • 白色对象:潜在的垃圾,其内存可能被垃圾收集器回收;
  • 黑色对象:活跃的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象;
  • 灰色对象:活跃的对象,存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象。

在垃圾收集器开始工作时,程序中不存在任何的黑色对象,垃圾收集的根对象会被标记成灰色,垃圾收集器只会从灰色对象集合中取出对象开始扫描,当灰色集合中不存在任何对象时,标记阶段就会结束。

三色标记法的工作流程可以归纳成以下几个步骤:

  1. 从灰色对象的集合中选择一个灰色对象将其标记成黑色。
  2. 将黑色对象指向的所有对象都标记成灰色,保证该对象和被该对象引用的对象都不会被回收;
  3. 重复上述两个步骤直到对象图中不存在灰色对象。

当三色的标记清除的标记阶段结束之后,应用程序的堆中就不存在任何的灰色对象,我们只能看到黑色的存活对象以及白色的垃圾对象,垃圾收集器可以回收这些白色的垃圾,下面是使用三色标记垃圾收集器执行标记后的堆内存,堆中只有对象 D 为待回收的垃圾:

因为用户程序可能在标记执行的过程中修改对象的指针,所以三色标记清除算法本身是不可以并发或者增量执行的,它仍然需要 STW,在如下所示的三色标记过程中,用户程序建立了从 A 对象到 D 对象的引用,但是因为程序中已经不存在灰色对象了,所以 D 对象会被垃圾收集器错误地回收。

混合写屏障

想要在并发或者增量的标记算法中保证正确性,我们需要达成以下两种三色不变性(Tri-color invariant)中的一种:

  • 强三色不变性:黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象;
  • 弱三色不变性:黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径。

上图分别展示了遵循强三色不变性和弱三色不变性的堆内存,遵循上述两个不变性中的任意一个,我们都能保证垃圾收集算法的正确性,而屏障技术就是在并发或者增量标记过程中保证三色不变性的重要技术。

垃圾收集中的屏障技术更像是一个钩子方法,它是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码,根据操作类型的不同,我们可以将它们分成读屏障(Read barrier)和写屏障(Write barrier)两种,因为读屏障需要在读操作中加入代码片段,对用户程序的性能影响很大,所以编程语言往往都会采用写屏障保证三色不变性。

Go 语言在 v1.8 组合 Dijkstra 插入写屏障和 Yuasa 删除写屏障构成混合写屏障,该写屏障会将被覆盖的对象标记成灰色并在当前栈没有扫描时将新对象也标记成灰色。

为了移除栈的重扫描过程,除了引入混合写屏障之外,在垃圾收集的标记阶段,我们还需要将创建的所有新对象都标记成黑色,防止新分配的栈内存和堆内存中的对象被错误地回收,因为栈内存在标记阶段最终都会变为黑色,所以不再需要重新扫描栈空间。

GC的时机

运行时通过runtime.gcTrigger.test方法决定是否要触发垃圾回收,当满足垃圾收集的基本条件时–允许垃圾收集、程序没有崩溃并且没有处于垃圾收集循环,该方法会根据三种不同的方式触发不同的检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (t gcTrigger) test() bool {
if !meestats.enablegc || panicking != 0 || gcphase != _GCoff {
return false
}
switch t.kind {
case gcTriggerHeap:
return memstats.heap_live >= memstats.gc_trigger
case gcTriggerTime:
if gcpercent < 0 {
return false
}
lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))
return lastgc != 0 && t.now-lastgc > forcegcperiod
case gcTriggerCycle:
return int32(t.n - owrk.cycles) > 0
}
return true
}

1、gcTriggerHeap :堆内存的分配达到达控制器计算的触发堆大小;
2、gcTriggerTime :如果一定时间内没有触发,就会触发新的循环,该出发条件由 runtime.forcegcperiod 变量控制,默认为 2 分钟;
3、gcTriggerCycle:如果当前没有开启垃圾收集,则触发新的循环;
4、runtime.gcpercent 是触发垃圾收集的内存增长百分比,默认情况下为 100,即堆内存相比上次垃圾收集增长 100% 时应该触发 GC,并行的垃圾收集器会在到达该目标前完成垃圾收集。

用于开启垃圾收集的方法 runtime.gcStart 会接收一个 runtime.gcTrigger 类型的结构,所有出现 runtime.gcTrigger 结构体的位置都是触发垃圾收集的代码:

  • runtime.sysmon 和 runtime.forcegchelper :后台运行定时检查和垃圾收集;
  • runtime.GC :用户程序手动触发垃圾收集;
  • runtime.mallocgc :申请内存时根据堆大小触发垃圾收集。