runtime

题目来源:一点资讯

答案:

1.理解goruntime

golang 的 runtime 在 golang 中的地位类似于 Java 的虚拟机,不过 go runtime 不是虚拟机. golang 程序生成可执行文件在指定平台上即可运行,效率很高, 它和 c/c++ 一样编译出来的是二进制可执行文件. 我们知道运行 golang 的程序并不需要主机安装有类似 Java 虚拟机之类的东西,那是因为在编译时,golang 会将 runtime 部分代码链接进去.

2.runtime核心内容

  1. 协程(goroutine)调度(并发调度模型)
  2. 垃圾回收(GC)
  3. 内存分配
  4. 使得 golang 可以支持如 pprof、trace、race 的检测
  5. 支持 golang 的内置类型 channel、map、slice、string等的实现
  6. 等等

下图是 golang 程序、runtime、可执行文件与操作系统之间的关系. 区别于 Java 需要安装虚拟机,go 语言的可执行文件已经包含了 golang 的 runtime,它为用户的 go 程序提供协程调度、内存分配、垃圾回收等功能.此外还会与系统内核进行交互,从而真正的利用好 CPU 等资源. 本文主要简单介绍 golang runtime 的并发调度模型、垃圾回收与内存分配.

3.协程调度模型

调度是操作系统的核心功能了,从计算机诞生以来,任务的调度就一直在不断改进与发展,以不断适应计算机的发展. 单任务、多任务、并发、并行等调度. 到如今云计算时代分布式调度也十分成熟,由 go 编写的 kubernetes 已经广泛用于各个公司的分布式集群中.

golang 语言相比其它语言有一个特殊之处,它实现了自己的调度模块,并不完全是由计算机操作系统进行调度的(进程、线程). golang 原生支持协程 goroutine,区别于线程、进程. goroutine 的调度由 go runtime 进行,这也是 golang 并发效率高的原因之一.

go 在处理协程上,使用了 GPM 调度模型,从而支持高效的并发调度. 如下图,内核线程与逻辑处理器是多对多的关系即 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 可以被执行.

4.垃圾回收(GC)

垃圾回收机制是编程语言的重要部分,它影响到程序的长久稳定运行. Java、Python 等语言都有自己的垃圾回收机制,而不需要像 c/c++一样由程序员管理,可以避免大量的内存泄漏.

4.1 常用的垃圾回收算法

  1. 引用计数(reference counting): Python 便主要采用的是引用计数的方式,每一个对象都会记录它的引用数,每当有新的引用则值增加,删除则减少,直到引用值为 0 ,则该对象的生命周期结束.
  2. 标记-清扫(mark & sweep): 使用标记清扫算法,未引用的对象并不会立刻被清除,而是被标记. 直到内存耗尽,挂起程序,清扫所有未被引用的对象,然后继续程序. 标记清扫法跟踪了 root 访问的所有对象,它可以有效的处理循环引用. 它有一个问题是需要 STW (stop the world). golang 便是采用的标记-清扫法进行垃圾回收.在 golang 的迭代过程中改进为**三色标记清扫法,**用来减少 STW 的影响.
  3. 复制收集(copy and collection): 目前许多商业虚拟机都采用这种垃圾回收算法. 它将内存分为两部分,只使用其中一部分,在进行垃圾回收时,将存活的对象复制到另一部分. 然后清理所有第一部分内存使其构成完成一块,从而避免内存碎片.

4.2 Golang 的三色标记法

golang 的垃圾回收是基于标记清扫算法,这种算法需要进行 STW(stop the world),这个过程就会导致程序是卡顿的,频繁的 GC 会严重影响程序性能. golang 在此基础上进行了改进,通过三色标记清扫法写屏障来减少 STW 的时间.

三色标记

三色标记法的流程如下,它将对象通过白、灰、黑进行标记,参考下图3的动图过程:

  1. 所有对象最开始都是白色.
  2. 从 root 开始找到所有可达对象,标记为灰色,放入待处理队列。
  3. 遍历灰色对象队列,将其引用对象标记为灰色放入待处理队列,自身标记为黑色。
  4. 循环步骤3直到灰色队列为空为止,此时所有引用对象都被标记为黑色,所有不可达的对象依然为白色,白色的就是需要进行回收的对象。

三色标记法相对于普通标记清扫,减少了 STW 时间. 这主要得益于标记过程是 “on-the-fly” 的,在标记过程中是不需要 STW 的,它与程序是并发执行的,这就大大缩短了 STW 的时间.

写屏障

当标记和程序是并发执行的,这就会造成一个问题. 在标记过程中,有新的引用产生,可能会导致误清扫. 清扫开始前,标记为黑色的对象引用了一个新申请的对象,它肯定是白色的,而黑色对象不会被再次扫描,那么这个白色对象无法被扫描变成灰色、黑色,它就会最终被清扫,而实际它不应该被清扫. 这就需要用到屏障技术,golang 采用了写屏障,作用就是为了避免这类误清扫问题. 写屏障即在内存写操作前,维护一个约束,从而确保清扫开始前,黑色的对象不能引用白色对象.

4.3 GC 触发条件

1.当前内存分配达到一定比例则触发

2.2 分钟没有触发过 GC 则触发 GC

3.手动触发,调用 runtime.GC()

5.内存分配

5.1 Tcmalloc 算法

Tcmalloc(Thread Caching Malloc) 是 google 为 c 语言开发的运行时内存分配算法. 其核心思想是多级管理,从而降低锁的粒度. Go runtime 的内存分配就采用了 Tcmalloc 算法.

5.2 golang 内存分配

Go 程序在启动时,会首先向系统申请一块内存(虚拟地址空间),然后自己切成小块进行管理. 将申请的内存,分成 3 个区域,spans、bitmap、arena,如下图4,这三个区域的作用如下.

  1. arena: 就是堆区,go runtime 在动态分配的内存都在这个区域,并且将内存块分成 8kb 的页,一些组合起来的称为 **mspan,**成为 go 中内存管理的基本单元,这种连续的页一般是操作系统的内存页几倍大小.
  2. bitmap: 顾名思义,用来标记堆区使用的映射表,它记录了哪些区域保存了对象,对象是否包含指针,以及 GC 的标记信息.
  3. spans: 存放 mspan 的指针,根据 spans 区域的信息可以很容易找到 mspan. 它可以在 GC 时更快速的找到的大块的内存 mspan.

    参与讨论