go gc垃圾回收

1,689 阅读5分钟

gc入口

  1. 手动出发:runtime.GC
  2. 内存使用达到某个阈值:runtime.mallocgc
  3. 后台有个goroutine每2min触发一次:forcegchelper

三色

黑色:已经扫描完毕,子节点扫描完毕。(gcmarckbits=1,且在队列外)
灰色:已经扫描完毕,子节点未扫描完毕。(gcmarckbits=1,且在队列内) 白色: 未扫描,collector不知道任何相关信息

三色主要是为了能让垃圾回收流程应用流程并发执行

GC roots

  1. 全局变量
  2. goroutine栈

GC发展史

v1.3-标记清除法

主要分为两个步骤:

  • 启动STW
  • mark标记
  • 停止STW
  • sweep清除
  1. 开启STW,停止程序运行,从root节点开始标记

image.png 2. 从root出发,标记所有可达对象

image.png 3. 停止STW,回收所有未被标记的对象

image.png 缺点:整个GC期间需要STW,性能太差

v.15 三色标记法

image.png

image.png

image.png

image.png

image.png

三色标记法如果仅仅是这样的话,仍然需要STW,但是为什么呢? 举一个例子:

image.png

image.png

image.png 本该存在的对象被垃圾回收掉,这可是bug!
这时你肯定会疑惑,这三色标记法有问题,加了STW影响性能,不加STW有bug,这也用不了啊。
那我们继续看go是如何解决这个问题的

在三色标记过程中,对象如果要缺失,要同时满足以下两点:

  1. 白色对象被黑色对象引用
  2. 灰色对象与白色对象间的关系遭到破坏

只要破环上面两个条件之一,就能保证对象不会丢失,所以golang团队提出了两种破环条件的方式:

  1. 强三色不变式
  2. 弱三色不变式

强三色不变式

不允许黑色对象引用白色对象

image.png

弱三色不变式

所有被黑色对象引用的白色对象,都有一条路径能够被灰色对象引用 image.png

为次,提出了两种实现机制:

  1. 插入写屏障
  2. 删除写屏障

插入写屏障

当A对象引用B对象时,将B标记为灰色

注意:插入写屏障只在堆中生效,不在站空间生效,因为go可能有很多gotoutine,都进行栈的写屏障保护会有性能问题

下面演示插入写屏障如何工作: image.png

image.png

image.png

由于堆区的插入写屏障机制,obj3得到保护,被标记为灰色。但是因为栈上没有插入写屏障机制,所以在扫描完成时,仍然可能存在栈上白色对象被黑色对象引用,所以最后需要在栈空间上STW,再扫描一次

image.png

image.png

image.png

image.png

所以插入写屏障,是在正常三色标记后,需要在栈上STW,并再扫描一次

删除写屏障

当删除引用时,被删除的对象是灰色或者白色,则被标记成灰色 下面演示删除写屏障如何工作:

image.png

image.png

image.png

image.png

此时我们发现,在栈上删除对象会触发删除写屏障机制,因为删除写屏障会在GC开始时,对整个栈做快照,从而达到拦截删除操作 但是删除写屏障有一个缺点,如下所示:

image.png

一个对象被删除后,虽然可能没有其他存活对象引用它,但是仍然在本轮GC中存活,会降低回收精度,增加下轮GC成本

总结

插入写屏障和删除写屏障都解决了单一三色标记法需要长时间STW的问题,并且goV1.5采用了插入写屏障+三色标记法作为golang的GC机制。
两种写屏障的对比:

  • 插入写屏障:
    • 栈上的操作无法拦截,最后需要对栈STW,再重新扫描一次
  • 删除写屏障:
    • GC开始时,会记录整个栈的快照,用来拦截操作
    • 回收精度低

V1.8混合写屏障机制

在gov1.8将两个机制的优点采纳,提出了混合写屏障机制:

  • GC刚开始的时候,将栈上所有可达对象全部标记为黑色
  • GC期间,任何在栈上创建的对象,均为黑色
  • 堆上被删除的对象标记为灰色
  • 堆上新增的对象被标记为灰色

我们来分析下过程:

image.png

image.png

场景1: 堆对象被删除引用,并被栈对象引用

image.png

场景2: 栈对象被删除引用,并被另一个栈对象引用

image.png

image.png

场景3: 堆对象被删除引用,并被另一个堆对象引用

image.png

image.png

image.png

场景4: 栈对象被删除引用,并被另一个堆对象引用

image.png

image.png

image.png

总结

混合写屏障机制:只在堆中生效,避免了最后对栈的STW以及re-scan,提升GC效率

GC标记简要过程

  1. 标记首先找到所有要标记的root:全局变量以及goroutine的栈
  2. 在标记过程中,会将表记的灰色对象放入gcWork中的wbuf1或wbuf2中,一个满了再放另一个,此时是无索的放。当两个都满了,就加锁往work的全局full里面放。
  3. 在标记过程中,goroutine和扫描goutine并发执行,所以会将此时修改的指针的指向前/后对象放到wbBuf里面
  4. 接下来唤醒清扫流程

GC清扫

// gcenable is called after the bulk of the runtime initialization,
// just before we're about to start letting user code run.
// It kicks off the background sweeper goroutine, the background
// scavenger goroutine, and enables GC.
func gcenable() {
   // Kick off sweeping and scavenging.
   gcenable_setup = make(chan int, 2)
   go bgsweep()
   go bgscavenge()
   <-gcenable_setup
   <-gcenable_setup
   gcenable_setup = nil
   memstats.enablegc = true // now that runtime is initialized, GC is okay
}

bgsweep: 负责清扫mspan,用gcmarkbits替换allocbits,把unswept的mspan变成swept的mspan bgscavenge: 负责奖内存归还给os

image.png

image.png

image.png

参考:
juejin.cn/post/704073…
segmentfault.com/a/119000002…