概述
垃圾回收 (Garbage Collection,简称 GC) 是编程语言中提供的主动的内存管理机制,主动开释不须要的内存对象,让出存储器资源。GC 过程中无需程序员手动执行。GC 机制在古代很多编程语言都反对,GC 能力的性能与优劣也是不同语言之间对比度指标之一。
Golang 在 GC 的演进过程中也经验了很屡次改革
Go V1.3 之前应用的是标记 - 革除 (mark and sweep) 算法
Go V1.5 开始应用三色并发标记法
Go V1.8 应用三色标记法 + 混合写屏障机制
标记 - 打扫算法 (mark-sweep)
标记革除算法是最常见的垃圾收集算法,标记革除收集器是跟踪式垃圾收集器,其执行过程能够分成标记 (Mark) 和革除(Sweep),算法流程如下:
- 标记阶段:暂停应用程序的执行,从根对象触发查找并标记堆中所有存活的对象;
- 革除阶段:遍历堆中的全副对象,回收未被标记的垃圾对象并将回收的内存退出闲暇链表,复原应用程序的执行;
操作非常简单,然而有一点须要额定留神:mark and sweep 算法在执行的时候,须要程序暂停(stop the world)。
三色标记算法
原始标记革除算法带来的长时间 STW, 为了解决这一问题,Go 从 V1.5 版本实现了基于三色标记革除的并发垃圾收集算法,在不暂停程序的状况下即可实现对象的可达性剖析,三色标记算法将程序中的对象分成红色、彩色和灰色三类,算法流程如下:
- 遍历根对象的第一层可达对象标记为灰色, 不可达默认红色。
- 将灰色对象的下一层可达对象标记为灰色, 本身标记为彩色。
- 多次重复步骤 2, 直到灰色对象为 0, 只剩下红色对象和彩色对象。
- 回收红色对象。
示例:
- 遍历根对象的第一层可达对象标记为灰色, 不可达默认红色
- 将灰色对象 A 的下一层可达对象标记为灰色, 本身标记为彩色
- 持续遍历灰色对象的上层对象, 反复步骤 2
- 持续遍历灰色对象的上层对象, 反复步骤 2
- 扫描完结后,回收所有红色的节点。
三色标记算法的问题
如果没有 STW,那么也就不会再存在性能上的问题,那么接下来咱们假如如果三色标记法不退出 STW 会产生什么事件?
咱们还是基于上述的三色并发标记法来说, 他是肯定要依赖 STW 的. 因为如果不暂停程序, 程序的逻辑扭转对象援用关系, 这种动作如果在标记阶段做了批改,会影响标记后果的正确性,咱们来看看一个场景,如果三色标记法, 标记过程不应用 STW 将会产生什么事件?
咱们把初始状态设置为曾经经验了第一轮扫描,目前彩色的有对象 1 和对象 4,灰色的有对象 2 和对象 7,其余的为红色对象,且对象 2 是通过指针 p 指向对象 3 的,如图所示。
当初如何三色标记过程不启动 STW,那么在 GC 扫描过程中,任意的对象均可能产生读写操作,如图所示,在还没有扫描到对象 2 的时候,曾经标记为彩色的对象 4,此时创立指针 q,并且指向红色的对象 3。
与此同时灰色的对象 2 将指针 p 移除,那么红色的对象 3 实则就是被挂在了曾经扫描实现的彩色的对象 4 下,如图所示。
而后咱们失常指向三色标记的算法逻辑,将所有灰色的对象标记为彩色,那么对象 2 和对象 7 就被标记成了彩色,如图所示。
那么就执行了三色标记的最初一步,将所有红色对象当做垃圾进行回收,如图所示。
然而最初咱们才发现,原本是对象 4 非法援用的对象 3,却被 GC 给“误杀”回收掉了。
能够看出,有两种状况,在三色标记法中,是不心愿被产生的。
条件 1: 一个红色对象被彩色对象援用(红色被挂在彩色下)
条件 2: 灰色对象与它之间的可达关系的红色对象受到毁坏(灰色同时丢了该红色)
如果当以上两个条件同时满足时,就会呈现对象失落景象! 在 Golang 比拟晚期的版本中,是应用 STW 的计划来保障一致性,这样做的害处是效率非常低。
三色一致性
STW 的过程有显著的资源节约,对所有的用户程序都有很大影响。那么是否能够在保障对象不失落的状况下正当的尽可能的进步 GC 效率,缩小 STW 工夫呢?
目前 Golang 应用通过引入三色一致性的机制,尝试去毁坏下面的两个必要条件就能够了,分为强三色一致性和弱三色一致性。
强三色不变性(strong tri-color invariant):彩色对象不会指向红色对象,只会指向灰色对象或者彩色对象。
弱三色不变性(weak tri-color invariant):即使彩色对象指向红色对象,那么从灰色对象登程,总存在一条能够找到该红色对象的门路。
写屏障
Golang 中应用三色一致性的办法是引入一个叫做写屏障的机制,来实现三色一致性,写屏障机制分为插入屏障和删除屏障。
插入屏障
具体操作: 在 A 对象援用 B 对象的时候,B 对象被标记为灰色。(将 B 挂在 A 上游,B 必须被标记为灰色)
删除屏障
具体操作: 被删除的对象,如果本身为灰色或者红色,那么被标记为灰色。
满足: 弱三色不变式. (爱护灰色对象到红色对象的门路不会断)
混合写屏障
插入写屏障和删除写屏障的尽管大大的缩短的零碎 GC 的 STW 工夫,然而也有其短板:
- 插入写屏障:完结时须要 STW 来从新扫描栈,标记栈上援用的红色对象的存活;
- 删除写屏障:回收精度低,GC 开始时 STW 扫描堆栈来记录初始快照,这个过程会爱护开始时刻的所有存活对象。
Go V1.8 版本引入了混合写屏障机制(hybrid write barrier),防止了对栈 re-scan 的过程,极大的缩小了 STW 的工夫。
具体操作:
- GC 开始将栈上的对象全副扫描并标记为彩色(之后不再进行第二次反复扫描,无需 STW),
- GC 期间,任何在栈上创立的新对象,均为彩色。
- 被删除的对象标记为灰色。
- 被增加的对象标记为灰色。
GC 工作机制
GC 触发机会
在 Go 中次要会在三个中央触发 GC:
- 监控线程 runtime.sysmon 定时调用;
- 手动调用 runtime.GC 函数进行垃圾收集;
- 申请内存时 runtime.mallocgc 会依据堆大小判断是否调用;
GC 流程剖析
当 GC 被触发后,Golang 开始执行 GC 循环,循环分为四个阶段别离是:清理终止,标记,标记终止,清理。
须要留神的是理论 runtime 的源码中只定义了 _GCoff / _GCmark / _GCmarktermination 三种状态,GC 敞开、清理终止和清理都对应 _GCoff 这一状态。
清理终止(sweep termination)
会触发 STW,所有的 P(处理器)都会进入 safe-point(平安点);清理未被清理的内存对象.
标记阶段(the mark phase)
将 GC 状态 从 _GCoff 改成 _GCmark,开启 Write Barrier(写入屏障)、mutator assists(帮助线程),将根对象入队;恢复程序执行,mark workers(标记过程)和 mutator assists(帮助线程)会开始并发标记内存中的对象。对于任何指针写入和新的指针值,都会被写屏障笼罩,而所有新创建的对象都会被间接标记成彩色;GC 执行根节点的标记,这包含扫描所有的栈、全局对象以及不在堆中的运行时数据结构。扫描 goroutine 栈绘导致 goroutine 进行,并对栈上找到的所有指针加置灰,而后继续执行 goroutine。
GC 在遍历灰色对象队列的时候,会将灰色对象变成彩色,并将该对象指向的对象置灰;
GC 会应用分布式终止算法(distributed termination algorithm)来检测何时不再有根标记作业或灰色对象,如果没有了 GC 会转为 mark termination(标记终止),
标记终止(mark termination)
STW,而后将 GC 阶段转为 _GCmarktermination, 敞开 GC 工作线程以及 mutator assists(帮助线程);执行清理,如 flush mcache。
清理阶段(the sweep phase)
将 GC 状态转变至 _GCoff,初始化清理状态并敞开 Write Barrier(写入屏障);恢复程序执行,从此开始新创建的对象都是红色的;后盾并发清理所有的内存治理单元。
源码剖析
func GC() {
// 期待上一轮 GC 循环完结
n := atomic.Load(&work.cycles)
gcWaitOnMark(n)
// 先实现第 N 轮 GC 循环,而后触发第 N + 1 轮 GC 循环
gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})
// 期待第 N + 1 轮 GC 循环的 mark termination 完结
gcWaitOnMark(n + 1)
// 清理未实现前,先出让以后执行机会
for atomic.Load(&work.cycles) == n+1 && sweepone() != ^uintptr(0) {
sweep.nbgsweep++
Gosched()}
for atomic.Load(&work.cycles) == n+1 && !isSweepDone() {Gosched()
}
// 实现清理后,进入标记阶段
mp := acquirem()
cycle := atomic.Load(&work.cycles)
if cycle == n+1 || (gcphase == _GCmark && cycle == n+2) {mProf_PostSweep()
}
releasem(mp)
}
对于 GC 的一些经验总结
事实上,尽管 Golang 的内存治理和 GC 机制曾经十分欠缺并尽可能减少其资源耗费,然而当你的程序处于一些极其的负载中,这种编译器托管形式仍免不了性能降落,因而在传统非 GC 语言中总结的一些教训诸如尽可能的复用变量,尽量应用指针传值代替变量复制传值等仍然实用。
Golang 特有的内存逃逸的问题可能是你的程序性能体现不佳的首恶;另外,因为 Golang 中 string 类型底层是常量数组,对它的批改也可能让你的程序性能极差。
更多技术分享浏览我的博客:
https://thierryzhou.github.io
本文由 mdnice 多平台公布