golang 垃圾回收(三)插入写屏障

Go语言精选

共 1666字,需浏览 4分钟

 ·

2020-07-27 19:50


  • 并发的垃圾回收

    • STW 安全的回收

    • 并发的垃圾回收

  • 插入写屏障

    • 伪代码

    • 对象丢失的必要条件

    • 写屏障是怎么解决问题?


并发的垃圾回收

golang 语言设计的根本性追求就是高并发,低延迟,所以golang 的垃圾回收也是持续在优化。golang 的垃圾回收是并发垃圾回收设计,业务运行和回收器运行并发,这种设计的初衷是降低垃圾回收停顿时间。

之前提过一个例子,如果你要安全的实现回收垃圾,那么简单的就是回收垃圾的时候,把所有的业务操作都停止,这个术语是STW(stop the world)。下面用一些术语:

  1. 赋值器:这个就是程序的业务代码
  2. 回收器:垃圾回收器

STW 安全的回收

下面画了一个图,表示了这种简单例子的一个演进:

  1. 灰色表示一次垃圾回收操作
  2. 黑色表示下一次


图里我们看到:

  1. 在单处理器的场景,赋值器和回收器的代码是交替运行的,回收器回收的时间即为赋值器停顿的时间;
  2. 多处理器的时候,多个赋值器线程并行执行,但是每次回收器回收的时候,还是要挂起多个赋值器;
  3. 第三种,就是让多个处理器并行的执行回收器的任务,减少停顿;

这是一个显而易见的实现和优化演进,其实再进一步,我们可以把完整的一个回收任务拆分成小粒度的,搞成一次次增量的回收,这样单次的停顿时间就更少了。

并发的垃圾回收

golang 明显不是这个(哈哈,曾经是),golang 必须要让赋值器和回收器并发起来,不能有明显的停顿。golang 当前的垃圾回收特点:

  1. 完全消除了明显的 STW (除了开启的垃圾回收的时候)
  2. 回收器标记、回收过程完全和赋值器并发
    1. 但是注意一点,对于单个栈来说,是一个一个挂起扫描的,这种扫描方式叫做 on-the-fly collection,并且是增量式的;

golang 的回收没有混合屏障之前,一直是插入写屏障,由于栈赋值没有 hook 的原因,所以有 STW,混合写屏障之后,就没有 STW。

这里有个点要理解:STW 是全局的赋值器挂起,我们一直说 golang 消除了 STW 说的是没有了全局性的挂起,但是局部的赋值器挂起是一直有的,包括现在也是有的。


插入写屏障

伪代码

Write 操作改变特定内存的值。改操作引发内存存储,需要三个参数:指向源的指针、待修改域的索引、待存储的值。写赋值操作用伪代码表示下:

Write(src, i, val):    src[i] <- val

我们的插入写屏障就是在这段赋值代码中,添加一段 hook 代码,这段 hook 代码就是所谓的屏障代码,由编译器在编译期生成。写屏障的实现有多种,golang 使用的是 Dijkstra 算法实现:

atomic Write(src, i, ref)    src[i] <- ref    if isBlack(src)        shade(ref)

这段伪代码我们非常容易看懂,就是加了后面的一个判读逻辑,如果 src 已经是黑色的,那么就把指向的新对象置灰色。

对象丢失的必要条件

之前的文章有提到三色标记法,提到,如果要想出现对象丢失(错误的回收)那么必须是同时满足两个条件:

  • 条件1:赋值器把白色对象的引用写入给黑色对象了(换句话说,黑色对象指向白色对象了)
  • 条件2:从灰色对象出发,最终到达该白色对象的所有路径都被赋值器破坏(换句话说,这个已经被黑色指向的白色对象,还没有在灰色对象的保护下)

图示举例:



  1. 赋值器操作一:X -> Z
  2. 赋值器操作二:Y -> null
  3. 回收器操作一:Scan Y
  4. 回收器操作二:回收 Z (这就有问题了)

在这两个条件同时出现的时候,才会出现对象被错误的回收。然后我们回过头看下写屏障的实现,就会发现,写屏障从根本上破坏了第一个条件的出现。

写屏障是怎么解决问题?

加了屏障的示意图:



插入写屏障就是这么简单。只要你保证时时刻刻没有黑色对象指向白色对象的条件出现,那么回收的正确性就能保证。但是话又说回来了,这个屏障是配合赋值器回收器并发的场景才需要,如果你允许直接STW执行回收器逻辑,那就不需要这么复杂了,当然啦,这样的话赋值器的性能肯定就不行啦。

虽然插入写屏障能解决问题,但是 golang 针对栈上对象的赋值却没有捕捉(没有生成写屏障),原因自然是性能损耗和实现复杂度的考虑。这就开了一个例外的口子,有一些黑色的栈对象指向了白色的对象,而回收器却无法感知到。golang 的解决方法是:最后再 STW 重新扫描一把栈。这个自然就会导致整个进程的赋值器卡顿,所以后面 golang 是引用混合写屏障解决这个问题。

混合写屏障混合的是谁?

混合的是删除写屏障,下一篇,聊删除写屏障是什么。




推荐阅读



学习交流 Go 语言,扫码回复「进群」即可


站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验


Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注


浏览 24
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报