Go Runtime 的总览
golang 的 runtime 在 golang 中的地位类似于 Java 的虚拟机,不过 go runtime 不是虚拟机. golang 程序生成可执行文件在指定平台上即可运行,效率很高, 它和 c/c++ 一样编译出来的是二进制可执行文件. 我们知道运行 golang 的程序并不需要主机安装有类似 Java 虚拟机之类的东西,那是因为在编译时,golang 会将 runtime 部分代码链接进去.
golang 的 runtime 核心功能包括以下内容:
- 协程(goroutine)调度(并发调度模型)
- 垃圾回收(GC)
- 内存分配
- 使得 golang 可以支持如 pprof、trace、race 的检测
- 支持 golang 的内置类型 channel、map、slice、string等的实现
- 等等
下图 1 是 golang 程序、runtime、可执行文件与操作系统之间的关系. 区别于 Java 需要安装虚拟机,go 语言的可执行文件已经包含了 golang 的 runtime,它为用户的 go 程序提供协程调度、内存分配、垃圾回收等功能.此外还会与系统内核进行交互,从而真正的利用好 CPU 等资源. 本文主要简单介绍 golang runtime 的并发调度模型、垃圾回收与内存分配.
协程调度模型
调度是操作系统的核心功能了,从计算机诞生以来,任务的调度就一直在不断改进与发展,以不断适应计算机的发展. 单任务、多任务、并发、并行等调度. 到如今云计算时代分布式调度也十分成熟,由 go 编写的 kubernetes 已经广泛用于各个公司的分布式集群中.
golang 语言相比其它语言有一个特殊之处,它实现了自己的调度模块,并不完全是由计算机操作系统进行调度的(进程、线程). golang 原生支持协程 goroutine,区别于线程、进程. goroutine 的调度由 go runtime 进行,这也是 golang 并发效率高的原因之一.
go 在处理协程上,使用了 GPM 调度模型,从而支持高效的并发调度. 如下图 2 ,内核线程与逻辑处理器是多对多的关系即 M:N. 从而提升并发效率. GPM 各个模块的解释如下:
- G: 即 Goroutine,更轻量级的线程,保存着上下文信息.
- P: Processor,是逻辑处理器. 将 goroutine 绑定逻辑处理器 P 的本地队列后,才会被调度. Processor 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等
- M: 它才是真正的计算资源,是系统线程.
- 全局队列(Global Run Queue): 未分配 Processor 的 Goroutine 保存在全局队列中. Processor 或 M 都可以从全局队列中取出 G .
- 本地队列(Local Run Queue): 是 Processor 的队列,当队列为空时,会从全局队列或其它队列补充 Goroutine.
- sysmon 协程: go runtime 会创建一个 sysmon 协程. 它会定期唤醒检查 goroutine 和 processor,确保 goroutine 不会长期占用 CPU 以及 Processor 可以被执行.
垃圾回收(GC)
垃圾回收机制是编程语言的重要部分,它影响到程序的长久稳定运行. Java、Python 等语言都有自己的垃圾回收机制,而不需要像 c/c++一样由程序员管理,可以避免大量的内存泄漏.
3.1 常用的垃圾回收算法
- 引用计数(reference counting): Python 便主要采用的是引用计数的方式,每一个对象都会记录它的引用数,每当有新的引用则值增加,删除则减少,直到引用值为 0 ,则该对象的生命周期结束.
标记-清扫(mark & sweep): 使用标记清扫算法,未引用的对象并不会立刻被清除,而是被标记. 直到内存耗尽,挂起程序,清扫所有未被引用的对象,然后继续程序. 标记清扫法跟踪了 root 访问的所有对象,它可以有效的处理循环引用. 它有一个问题是需要 STW (stop the world). golang 便是采用的标记-清扫法进行垃圾回收.在 golang 的迭代过程中改进为三色标记清扫法,用来减少 STW 的影响.
复制收集(copy and collection): 目前许多商业虚拟机都采用这种垃圾回收算法. 它将内存分为两部分,只使用其中一部分,在进行垃圾回收时,将存活的对象复制到另一部分. 然后清理所有第一部分内存使其构成完成一块,从而避免内存碎片.
3.2 Golang 的三色标记法
golang 的垃圾回收是基于标记清扫算法,这种算法需要进行 STW(stop the world),这个过程就会导致程序是卡顿的,频繁的 GC 会严重影响程序性能. golang 在此基础上进行了改进,通过三色标记清扫法与写屏障来减少 STW 的时间.
三色标记
三色标记法的流程如下,它将对象通过白、灰、黑进行标记,参考下图3的动图过程:
- 所有对象最开始都是白色.
- 从 root 开始找到所有可达对象,标记为灰色,放入待处理队列。
- 遍历灰色对象队列,将其引用对象标记为灰色放入待处理队列,自身标记为黑色。
- 循环步骤3直到灰色队列为空为止,此时所有引用对象都被标记为黑色,所有不可达的对象依然为白色,白色的就是需要进行回收的对象。
三色标记法相对于普通标记清扫,减少了 STW 时间. 这主要得益于标记过程是 “on-the-fly” 的,在标记过程中是不需要 STW 的,它与程序是并发执行的,这就大大缩短了 STW 的时间.
写屏障
当标记和程序是并发执行的,这就会造成一个问题. 在标记过程中,有新的引用产生,可能会导致误清扫,即将被清扫的标记为黑色的对象引用了白色的对象. 这就需要用到屏障技术,golang 采用了写屏障,作用就是为了避免这类误清扫问题. 写屏障即在内存写操作前,维护一个约束,从而确保将被清扫的黑色对象不能引用白色对象.
3.3 GC 触发条件
- 当前内存分配达到一定比例则触发
- 2 分钟没有触发过 GC 则触发
- GC 手动触发,调用 runtime.GC()
内存分配
4.1 Tcmalloc 算法
Tcmalloc(Thread Caching Malloc) 是 google 为 c 语言开发的运行时内存分配算法. 其核心思想是多级管理,从而降低锁的粒度. Go runtime 的内存分配就采用了 Tcmalloc 算法.
4.2 golang 内存分配
Go 程序在启动时,会首先向系统申请一块内存(虚拟地址空间),然后自己切成小块进行管理. 将申请的内存,分成 3 个区域,spans、bitmap、arena,如下图4,这三个区域的作用如下.
- arena: 就是堆区,go runtime 在动态分配的内存都在这个区域,并且将内存块分成 8kb 的页,一些组合起来的称为 mspan,成为 go 中内存管理的基本单元,这种连续的页一般是操作系统的内存页几倍大小.
- bitmap: 顾名思义,用来标记堆区使用的映射表,它记录了哪些区域保存了对象,对象是否包含指针,以及 GC 的标记信息.
- spans: 存放 mspan 的指针,根据 spans 区域的信息可以很容易找到 mspan. 它可以在 GC 时更快速的找到的大块的内存 mspan.
参考资料
-
深入浅出Golang Runtime[https://zhuanlan.zhihu.com/p/95056679]
-
golang中的runtime包教程[https://studygolang.com/articles/13994?fr=sidebar]
-
Go垃圾回收机制剖析[http://www.pianshen.com/article/168671039/]
-
12 Go 并发调度器模型[https://www.jianshu.com/p/5df0a7e118d8]
-
三色标记[https://studygolang.com/articles/12062]
-
图解Go语言内存分配[https://zhuanlan.zhihu.com/p/59125443]
-
图解 TCMalloc[https://zhuanlan.zhihu.com/p/29216091]
-
Visualizing memory management in Golang[https://deepu.tech/memory-management-in-golang/]
-
Go: What Does a Goroutine Switch Actually Involve?[https://medium.com/a-journey-with-go/go-what-does-a-goroutine-switch-actually-involve-394c202dddb7]
来源:CSDN
作者:皮拉图斯
链接:https://blog.csdn.net/chwlfg/article/details/104692651