垃圾回收
:Garbage Collection
简称GC
是编程语言中提供的内存管理功能。
常用垃圾回收算法手段
这一部分八股文内容一笔带过,做一个知识了解即可。
1、引用计数
对象被使用这个对象被引用计数+1,解除引用-1,引用数为0时就是垃圾,被回收。
若两个对象相互引用存在无法归零的可能,需要有相应的解决方案。
2、比较压缩
第一种整块复制:有两块相同的内存待清扫区域A和空白干净区域B,清扫A区域时将A区域有用的对象到B区域,然后清理整块A区域。
第二种存活临近复制:没有所谓AB两块区域,仅在A区域扫描到存活对象后复制到相似对象临近,相当于加入了一个碎片整理功能。
3、分代回收
这种算法基于一个概率论:大部分对象会从产生开始在很短的时间内变成垃圾,而存在的很长时间的对象往往都有较长的生命周期。
高频对新生成的对象进行回收称为「小回收」,低频对所有对象回收称为「大回收」。每一次「小回收」过后,就把存活下来的对象归为老年代,「小回收」的时候,遇到老年代直接跳过。基于上述概率论小回收中垃圾的比例较大。
如果在某个新生代的对象中,存在「老生代」的对象对它的引用,它就不是垃圾了,那么怎么制止「小回收」对其回收呢?这里用到了一种叫做写屏障(Write Barrier)的处理方式。
4、标记清扫
内存对象往往是相互关联的,从根对象往下连,标记清扫的理论基点:可达性近似等于存活性这种设定。
该方法分为两步,标记从根对象开始标记,对能够通过引用关联访问到的对象标记为存活;标记完成后那些没有关联到也就是不可达的对象就是垃圾,进行回收。
标记清扫有个极大的缺点就是标记清扫阶段需要暂停整个程序(Stop the world,简称STW)仅执行GC逻辑。
go语言的垃圾会收就是该种方式,只不过深度使用了协程的并发特性并且配合使用了混合写屏障技术。
三色标记抽象
三色标记抽象:
白色
:垃圾对象;
灰色
:对象还需要再次可达性分析标记,再次可达性分析后最终会变为黑色;
黑色
:存活对象;
三色标记抽象过程大致如下:
- 标记开始时所有对象为白色;
- 把直接能追踪到的根对象标记为灰色,灰色表示基于当前节点的追踪标记尚未完成;
- 基于灰色节点进行追踪,找到下一层关联的白色节点,标记灰色节点为黑色白色节点为灰色;
- 重复基于灰色节点的追踪流程并标记,最终只有黑色节点和白色节点,黑色就是存活对象白色就是垃圾可以在清理阶段执行清理;
三色不变式
垃圾回收器的正确性体现在:不应出现对象的丢失,也不应错误的回收还不需要回收的对象。三色不变式是三色标记过程中保障标记正确性的重要理论:
强三色不变式
:黑色对象不会指向白色对象,只能指向灰色或黑色对象;
弱三色不变式
:黑色对象可以指向白色对象,但此白色对象链路的上游中必须有一个灰色对象;
算法理论已证明只要满足上述任意一种不变式,就能保障对象不会被错误的回收。而屏障机制就是标记过程中保证三色不变式的重要技术实现。
屏障机制
插入写屏障
:对象被引用时触发,白色对应被引用到黑色对象时白色对象标记为灰色,可满让标记过程足强三色不变式;
删除写屏障
:对象被删除时触发,被删除的对象如果自身为白色或灰色直接标记为会色,可让标记过程满足若弱三色不变式;
go语言gc进化流程
go语言的垃圾回收gc一直在持续不断的优化。以下列出版本迭代情况的摘抄:
- go1.0:完全串行的标记和清除过程,需要暂停整个程序;
- go1.1:在多核主机并行执行垃圾收集的标记和清除阶段;
- go1.3:运行时基于只有指针类型的值包含指针的假设增加了对栈内存的精确扫描支持,实现了真正精确的垃圾收集;将 unsafe.Pointer 类型转换成整数类型的值认定为不合法的,可能会造成悬挂指针等严重问题;
- go1.5:实现了基于三色标记清扫的并发垃圾收集器;大幅度降低垃圾收集的延迟从几百 ms 降低至 10ms 以下;计算垃圾收集启动的合适时间并通过并发加速垃圾收集的过程;
- go1.6:实现了去中心化的垃圾收集协调器;基于显式的状态机使得任意 Goroutine 都能触发垃圾收集的状态迁移;使用密集的位图替代空闲链表表示的堆内存,降低清除阶段的 CPU 占用;
- go1.7:通过并行栈收缩将垃圾收集的时间缩短至 2ms 以内;
- go1.8:使用混合写屏障将垃圾收集的时间缩短至 0.5ms 以内;
- go1.9 :彻底移除暂停程序的重新扫描栈的过程;
- go1.10:更新了垃圾收集调频器(Pacer)的实现,分离软硬堆大小的目标;
- go1.12 :使用新的标记终止算法简化垃圾收集器的几个阶段;
- go1.13 :通过新的 Scavenger 解决瞬时内存占用过高的应用程序向操作系统归还内存的问题;
-
go1.14:使用全新的页分配器优化内存分配的速度;
三色标记和混合写屏障gc
上面介绍了三色抽象和屏障机制,go语言实现的三色标记+混合写屏障集中了插入屏障和删除屏障的优点,这种屏障称之为混合写屏障。go语言标记过程因混合写屏障存在要点如下:
- 标记开始是栈上的可达对象全部标记为黑色,因为栈上没有实现屏障;
- 在栈上创建的新对象均为黑色;
- 混合写屏障保障任何被删除的对象被标记为灰色,这是删除屏障的优点;
- 混合写屏障保障被添加的对象被标记为灰色,这是插入屏障的优点;
- 按可达性执行三色标记最终只有黑色和白色;
go语言gc流程和我们的业务代码流程是间隔交替并发执行的,参考go源码runtime包中的mgc.go文件的相关注释(见参考资料⑥),gc执行的流程按执行周期可划分为4个阶段:
第一次gc的准备阶段,所有P启动一个p.gcBgMarkWorkder
协程等待调度执行;这些协程就是标记阶段被P所调度执行的G。
1、清理终止阶段(sweep termination)
上一轮的gc过程需要彻底结束,清理尚未清理完的上一轮的白色区域。
- STW,每个P进入GC safepoint(安全点);
- 清理未被清理的span,如果GC被强制执行时才会出现这些未清理的span;
2、标记阶段(mark)
-
将gc标记从
_GCoff
修改为_GCmark
,开启写屏障(write barries
)和 协助助手(mutator assists
),将根对象放入队列。 在STW期间,在所有P都启用写屏障之前不会有什么对象被扫描。 - 禁用STW,标记工作线程和协助助手并发的执行。对于任何指针的写操作和指针值,都会被写屏障覆盖,使新分配的对象标记为黑色。
- GC执行根标记工作。包括扫描所有的栈,全局对象和不在堆数据结构中的堆指针。每扫描一个栈就会导致goroutine停止,把在栈上找到的所有指针置灰色,然后再恢复goroutine运行;
- GC 遍历队列中的每个灰色对象,扫描完以后将灰色对象标记为黑色,并将其指向的对象标记为灰色;
-
GC的标记过程是是分布式并发执行,采用了一种“分布式终止算法(
distributed termination algorithm
)” 来检测什么时候没有根对象或灰色对象。到这个时间点GC会转为标记中止(mark termination
)阶段。
----
用户程序协助(Mutator Assists)
本质就是业务代码的协程,只不过在gc标记阶段当Mutator Assists处于开启状态时如果专门用于标记的G也就是p.gcBgMarkWorkder
协程任务过于繁忙,用户程序协程G被P调度执行时也会分担一些标记任务。
----
3、标记终止阶段(mark termination)
- 启用STW;
-
gc切换至
_GCmarktermination
状态,并关闭辅助标记的用户程序即Mutator Assists; - 清理P上的线程缓存;
4、清理阶段(sweep)
-
gc切换至
_GCoff
状态,开始清理阶段,初始化清理状态并关闭写屏障; - 禁用STW,此后所有新创建的对象会被标记成白色留待下一次的gc执行扫描和清扫;
- 当有用户协程G申请新的内存管理单元时就会触发运行时创建的清扫协程G并发增量的执行清扫;
----
当前go最新版本为1.17.1,go语言的的GC经过多个版本的迭代优化,网络上流传的一张老版本go语言gc流程图如下,该图是基于1.8及其以前版本的GC,图中mark termination
阶段还有一个重新扫描(re-scan)全局的或有变更的栈区域的过程,go1.9中已经彻底移除这一过程,所以下图对go1.9及其以后版本已不再正确(参考资料⑦)。
参考资料:
① https://segmentfault.com/a/1190000018161588
② https://zhuanlan.zhihu.com/p/92210761
③ https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/
④ https://blog.csdn.net/qiya2007/article/details/107622603
⑤ https://blog.haohtml.com/archives/25372
⑥ https://github.com/golang/go/blob/a83a5587331392fc9483d183e446586b463ad8aa/src/runtime/mgc.go#L24-L82
⑦ https://github.com/golang/proposal/blob/master/design/17503-eliminate-rescan.md
哟嚯,本文评论功能关闭啦~