Go 语言如何实现垃圾回收中的 Stop the World (STW)

栏目: IT技术 · 发布时间: 4年前

内容简介:本篇文章讨论实现原理基于 Go 1.13.在垃圾回收机制 (GC) 中,"Stop the World" (STW) 是一个重要阶段。 顾名思义, 在 "Stop the World" 阶段, 当前运行的所有程序将被暂停, 扫描内存的 root 节点和添加写屏障 (write barrier) 。 本篇文章讨论的是, "Stop the World" 内部工作原理及我们可能会遇到的潜在风险。

Go 语言如何实现垃圾回收中的 Stop the World (STW)

本篇文章讨论实现原理基于 Go 1.13.

在垃圾回收机制 (GC) 中,"Stop the World" (STW) 是一个重要阶段。 顾名思义, 在 "Stop the World" 阶段, 当前运行的所有程序将被暂停, 扫描内存的 root 节点和添加写屏障 (write barrier) 。 本篇文章讨论的是, "Stop the World" 内部工作原理及我们可能会遇到的潜在风险。

Stop The World(STW)

这里面的"停止", 指的是停止正在运行的 goroutines。 下面这段程序就执行了 "Stop the World":

func main() {
   runtime.GC()
}

这段代码中, 调用 runtime.GC() 执行垃圾回收, 会触发 "Stop the World"的三个步骤。

(关于关于垃圾回收机制, 可以参考我的另外一篇文章 "Go: 内存标记在垃圾回收中的实现" ):

这个阶段的第一步, 是抢占所有正在运行的 goroutine(即图中 G ):

Go 语言如何实现垃圾回收中的 Stop the World (STW)

被抢占之后, 这些 goroutine 会被悬停在一个相对安全的状态。 同时,承载 Goroutine 的处理器 P (无论是正在运行代码的处理器还是已在 idle 列表中的处理器), 都会被被标记成停止状态 (stopped), 不再运行任何代码:

Go 语言如何实现垃圾回收中的 Stop the World (STW)

接下来, Go 调度器 (Scheduler) 开始调度, 把每个处理器的 Marking Worker (即图中 M ) 从各自对应的处理器 P 分离出来, 放到 idle 列表中去, 如下图:

Go 语言如何实现垃圾回收中的 Stop the World (STW)

在停止了处理器和 Marking Worker 之后, 对于 Goroutine 本身, 他们会被放到一个全局队列中等待:

Go 语言如何实现垃圾回收中的 Stop the World (STW)

到目前为止, 整个"世界"被停止. 至此, 仅存的 "Stop The World" (STW)goroutine 可以开始接下来的回收工作, 在一些列的操作结束之后, 再启动整个"世界"。

我们也可以在 Tracing 工具中看到一次 STW 的运行状态:

Go 语言如何实现垃圾回收中的 Stop the World (STW)

系统调用

下面我们来讨论一下 STW 是如何处理系统调用的。

我们知道, 系统调用是需要返回的, 那么当整个"世界"被停止的时候, 已经存在的系统调用如何被处理呢?

我们通过一个实际例子来理解:

func main() {
   var wg sync.WaitGroup
   wg.Add(10)
   for i := 0; i < 10; i++ {
      Go func() {
         http.Get(`https://httpstat.us/200`)
         wg.Done()
      }()
   }
   wg.Wait()
}

这是一段简单的系统调用的程序, 我们通过 Tracing 工具看一下它是如何被处理的:

Go 语言如何实现垃圾回收中的 Stop the World (STW)

我们可以看到, 这个系统调用 goroutine (即图中 G30 ) 在"世界"被停止的时候, 就已经存在了。

但是, 我们之前提到, STW 把所有的处理器 P 都标为停止状态 (stopped) , 所以, 这个系统调用的 Goroutine 也会被放到全局队列中, 等待 golang 世界恢复之后, 被重新启用。

延迟

前文提到 STW 的第三步是将 Marking Worker( M ) 从处理器( P )上分离, 然后放入 idle 列表中。

而实际上, Go 会等待他们自发停止, 也就是说当调度器(scheduler)运行的时候, 系统调用在运行的时候, STW 会等待。

理论上, 等待一个 Goroutine 被抢占是很快的, 但是在有些情况下, 还是会出现相应的延迟。

我们通过一个例子来模拟类似情况:

func main() {
   var t int
   for i := 0;i < 20 ;i++  {
      Go func() {
         for i := 0;i < 1000000000 ;i++ {
            t++
         }
      }()
   }

   runtime.GC()
}

我们还是来看一下这段代码运行的 Tracing 情况, 从下图我们可以看到 STW 阶段总共耗时 2.6 秒:

Go 语言如何实现垃圾回收中的 Stop the World (STW)

我们来简单分析为什么会出现这么长的 STW: 正如例子中的 main 函数, 一个没有函数调用的 Goroutine 一般不会被抢占, 那么这个 Goroutine 对应的处理器 P 在任务结束之前不会被释放。

而 STW 的机制是等待它自发停止, 因此就出现了 2.6 秒的 STW。

为了提高整体程序的效率, 我们一般需要避免或者改进这种情况。

关于这部分, 大家可以参考我的另一篇文章 "Go: Goroutine 和抢占"


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JAVA核心技术(卷1)

JAVA核心技术(卷1)

Cay S. Horstmann、Gary Cornell / 杜永萍、邝劲筠、叶乃文 / 机械工业出版社 / 2008-6 / 98.00元

《JAVA核心技术(卷1):基础知识(原书第8版)》是《Java核心技术》的最新版,《Java核心技术》出版以来一直畅销不衰,深受读者青睐,每个新版本都尽可能快地跟上Java开发工具箱发展的步伐,而且每一版都重新改写了的部分内容,以便适应Java的最新特性。本版也不例外,它反遇了Java SE6的新特性。全书共14章,包括Java基本的程序结构、对象与类、继承、接口与内部类、图形程序设计、事件处理......一起来看看 《JAVA核心技术(卷1)》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具