golang--GC(Garbage Collector) 垃圾回收
一.前言
在golang内存管理中我们一起探究了 golang 分配内存的过程,与 c 语言不同,golang 不需要人为的进行内存的分配和回收,在学会了 golang 分配内存以后,让我们一起来学习一下 golang 是如何进行内存回收的。
二.核心知识点
2.1 三色标记法
2.1.1 三色含义
白色对象 — 潜在的垃圾,其内存可能会被垃圾收集器回收;
黑色对象 — 活跃的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象;
灰色对象 — 活跃的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;
2.1.2 根对象
垃圾回收中最先检测的对象,包括以下三种对象
全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。
2.1.3 工作原理
在垃圾收集器开始工作时,程序中不存在任何的黑色对象,垃圾收集的根对象会被标记成灰色,垃圾收集器只会从灰色对象集合中取出对象开始扫描,当灰色集合中不存在任何对象时,标记阶段就会结束。
具体步骤如下:
当垃圾回收开始的时候只有白色对象,开始以后标记根对象(cpu 寄存器,全局变量)为灰色,从灰色对象的集合中选择一个灰色对象并将其标记成黑色;
将黑色对象指向的所有对象都标记成灰色,保证该对象和被该对象引用的对象都不会被回收;
重复上述两个步骤直到对象图中不存在灰色对象;
全部标记后系统中只有 D 没有被标记,所以 D 是可回收的
标记过程中根对象和三色的关联如下图所示
2.2 STW(StopTheWorld)
2.2.1 解释
垃圾回收会暂停所有应用程序线程。这种暂停被称为停止世界 (STW) 暂停。
2.2.2 原因
垃圾回收器的正确性体现在:不应出现对象的丢失,也不应错误的回收还不需要回收的对象。
STW 的目的是为了保证实现的正确性、防止无止境的内存增长等问题而不可避免的需要停止赋值器进一步操作对象图的一段过程。
如果我们考虑并发的用户态代码,回收器不允许同时停止所有赋值器,就是涉及了存在的多个不同状态的赋值器。为了对概念加以明确,还需要换一个角度,把回收器视为对象,把赋值器视为影响回收器这一对象的实际行为(即影响 GC 周期的长短),从而引入赋值器的颜色:
黑色赋值器:已经由回收器扫描过,不会再次对其进行扫描。
灰色赋值器:尚未被回收器扫描过,或尽管已经扫描过但仍需要重新扫描。
赋值器的颜色对回收周期的结束产生影响:
如果某种并发回收器允许灰色赋值器的存在,则必须在回收结束之前重新扫描对象图。
如果重新扫描过程中发现了新的灰色或白色对象,回收器还需要对新发现的对象进行追踪,但是在新追踪的过程中,赋值器仍然可能在其根中插入新的非黑色的引用,如此往复,直到重新扫描过程中没有发现新的白色或灰色对象。
于是,在允许灰色赋值器存在的算法,最坏的情况下,回收器只能将所有赋值器线程停止才能完成其跟对象的完整扫描,也就是我们所说的 STW。
2.3 写屏障
了解了 STW(StopTheWorld)之后,很明显可以得出结论 STW 的时间越长,对用户的影响越大,为了缩短 STW 的时间,写屏障应运而生,它保障了代码描述中对内存的操作顺序 既不会在编译期被编译器进行调整,也不会在运行时被 CPU 的乱序执行所打乱 。
2.3.1 弱三色不变性
当以下两个条件同时满足时会破坏垃圾回收器的正确性 [Wilson, 1992]:
条件 1: 赋值器修改对象图,导致某一黑色对象引用白色对象;
条件 2: 从灰色对象出发,到达白色对象的、未经访问过的路径被赋值器破坏。
只要能够避免其中任何一个条件,则不会出现对象丢失的情况,因为:
如果条件 1 被避免,则所有白色对象均被灰色对象引用,没有白色对象会被遗漏;
如果条件 2 被避免,即便白色对象的指针被写入到黑色对象中,但从灰色对象出发,总存在一条没有访问过的路径,从而找到到达白色对象的路径,白色对象最终不会被遗漏。
我们将满足条件一但是不满足条件二的情况称为弱三色不变性,弱三色不变形的好处在于:只要存在未访问的能够到达白色对象的路径,就可以将黑色对象指向白色对象。
2.3.2 Dijkstra 插入屏障
对于插入到黑色对象中的白色指针,无论其在未来是否会被赋值器删除,该屏障都会将其标记为可达(着色)。 在这种思想下,避免满足条件 1 的出现。
流程图如下
Dijkstra 插入屏障的好处在于可以立刻开始并发标记,但由于产生了灰色赋值器,缺陷是需要标记终止阶段 STW 时进行重新扫描。--》golang 需要二次 stw 的原因
2.3.3 Yuasa 删除屏障
其思想是当赋值器从灰色或白色对象中删除白色指针时,通过写屏障将这一行为通知给并发执行的回收器。 这一过程很像是在操纵对象图之前对图进行了一次快照。
如果一个指针位于波面之前,则删除屏障会保守地将目标对象标记为非白色存活对象,进而避免条件 2 来满足弱三色不变性。
Yuasa 删除屏障 [Yuasa, 1990] 在回收过程中,对于被赋值器删除最后一个指向这个对象导致该对象不可达的情况, 仍将其对象进行着色
流程图如下
2.3.4 混合写屏障
在诸多屏障技术中,Go 使用了 Dijkstra 与 Yuasa 屏障的结合, 即混合写屏障(Hybrid write barrier)技术。
基本思想:对正在被覆盖的对象进行着色,且如果当前栈未扫描完成, 则同样对指针进行着色。
为了防止丢失从灰色对象到白色对象的路径,应该假设 *slot
可能会变为黑色,为了确保 ptr
不会在被赋值到 *slot
前变为白色, shade(*slot)
会先将 *slot
标记为灰色,进而该写操作总是创造了一条灰色到灰色或者灰色到白色对象的路径,进而避免了条件
三.详细介绍
3.1 golang 三色标记法
golang 的垃圾回收采用的是 标记-清理(Mark-and-Sweep) 算法就是先标记出需要回收的内存对象快,然后清理,使用就是三色标记法。
3.1 gc 内存泄漏
根本原因
预期的能很快被释放的内存由于附着在了长期存活的内存上,或生命期意外的被延长,导致预计能够立即回收的内存长时间得不到回收(由于 goroutine 还有多种形式)
3.1.1 预期能被快速释放的内存因被根对象引用而没有得到迅速释放
当有一个全局对象时,可能不经意间将某个变量附着在其上,且忽略的将其进行释放,则该内存永远不会得到释放
3.1.2 goroutine 泄露
Goroutine 作为一种逻辑上理解的轻量级线程,需要维护执行用户代码的上下文信息。在运行过程中也需要消耗一定的内存来保存这类信息,而这些内存在目前版本的 Go 中是不会被释放的。因此,如果一个程序持续不断地产生新的 goroutine、且不结束已经创建的 goroutine 并复用这部分内存,就会造成内存泄漏的现象,
这种形式的 goroutine 泄漏还可能由 channel 泄漏导致。而 channel 的泄漏本质上与 goroutine 泄漏存在直接联系。Channel 作为一种同步原语,会连接两个不同的 goroutine,如果一个 goroutine 尝试向一个没有接收方的无缓冲 channel 发送消息,则该 goroutine 会被永久的休眠,整个 goroutine 及其执行栈都得不到释放,
3.2 gc 标记流程
3.2.1 流程介绍
gcphase 的三个状态
_GCoff // GC not running; sweeping in background, write barrier disabled
_GCmark // GC marking roots and workbufs: allocate black, write barrier ENABLED
_GCmarktermination // GC mark termination: allocate black, P's help GC, write barrier ENABLED
3.2.1.1 GC 执行扫描和终止
a. 暂停整个程序(stop the world),等待所有 goroutine 到达 GC 安全点(程序执行期间的一个点,在此点上所有 GC 根都是已知的,并且所有堆对象内容都是一致的。从全局的角度来看,所有线程都必须在安全点阻塞,然后 GC 才能运行。)
b.清除任何未经清除的 span,只有在预期时间之前强制执行此 GC 周期时,才会有未清除的 span。
3.2.1.2 GC 的标记阶段
a.为了标记阶段将 gcphase 从_GCoff 设置成_GCmark,开启写屏障(详情见备注),启用 mutator assist,将根标记任务放入队列。通过 STW 保证没有对象会被扫描,直到所有协程(Ps)启用写屏障。
b.唤醒程序(start the world),从此开始,GC 的工作由调度程序启用的 mark workers 和 allocation 一部分的 assists performed 执行,写屏障将任何指针指向的新指针和覆盖指针都标记为灰,新申请的对象立即判为黑色。
c.gc 执行根标记任务,这包括扫描所有的栈,为所有全局变量标灰色,以及对堆外运行时数据结构中的任何堆指针进行标灰色。扫描栈会停止 goroutine,为 goroutine 指针指向的所有栈着灰色,然后再重启 goroutine
d.GC 排出灰色对象的工作队列,将每个灰色对象扫描为黑色,并对在该对象中找到的所有指针标记为灰色(这反过来又可能将这些指针添加到工作队列中)。
e.由于 GC work 分散在本地缓存中,因此 GC 使用分布式终止算法来检测何时不再有根标记作业或灰色对象(参见 gcMarkDone 函数)。此时,GC 状态转换到标记终止(gcMarkTermination)。
3.2.1.3 GC 执行标记终止
a.暂停程序(stop the world)
b.设置 gcphase 状态到_GCmarktermination,停止 workers 和 assists
c.清理工作,如回收 mcaches 内存
3.2.1.4 GC 执行清除阶
a.设置 gcphase 到_GCoff,设置清除状态并禁止写屏障。
b.唤醒程序(start the world),从此时开始,新申请的对象为白色,若必要可以在使用前清扫 spans。
c.gc 在后台执行回收白色对象并响应内存的分配
3.2.1.5 当内存分配足够多,则循环开始
3.2.1.6 总结
golang 的 gc 一共是用了两次 STW
第一次目的是标记准备阶段,为并发标记做准备工作,启动写屏障
第二次目的是标记终止阶段,保证一个周期内标记任务完成,停止写屏障
3.3 gc 触发时机
Go 语言中对 GC 的触发时机存在两种形式:
主动触发,通过调用 runtime.GC 来触发 GC,此调用阻塞式地等待当前 GC 运行完毕。
被动触发,分为两种方式:
使用系统监控,当超过两分钟没有产生任何 GC 时,强制触发 GC。
使用步调(Pacing)算法,其核心思想是控制内存增长的比例。
参考学习
https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/
https://golang.design/under-the-hood/zh-cn/part2runtime/ch08gc/basic/
版权声明: 本文为 InfoQ 作者【en】的原创文章。
原文链接:【http://xie.infoq.cn/article/47929ee2c9becda801ba9e424】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论