垃圾回收
1.垃圾回收
01.三种常见垃圾回收机制
1.0 垃圾回收是什么
- 传统的系统级编程语言(主要指C/C++)中,程序员必须对内存小心的进行管理操作,控制内存的申请及释放。
- 稍有不慎,就可能产生内存泄露问题,这种问题不易发现并且难以定位
- 后来开发出来的几乎所有新语言(java,python,php等等)都引入了语言层面的自动内存管理
- 也就是语言的使用者只用关注内存的申请而不必关心内存的释放
- 内存释放由虚拟机(virtual machine)或运行时(runtime)来自动进行管理
- 而这种对不再使用的内存资源进行自动回收的行为就被称为垃圾回收。
1.1 引计数
- 原理
- 当一个对象的引用被创建或者复制时,对象的引用计数加1;当一个对象的引用被销毁时,对象的引用计数减1.
- 当对象的引用计数减少为0时,就意味着对象已经再没有被使用了,可以将其内存释放掉。
- 优点
- 引用计数有一个很大的优点,即实时性,任何内存,一旦没有指向它的引用,就会被立即回收,而其他的垃圾收集技术必须在某种特殊条件下才能进行无效内存的回收。
- 缺点
- 引用计数机制所带来的维护引用计数的额外操作与Python运行中所进行的内存分配和释放,引用赋值的次数是成正比的,
- 显然比其它那些垃圾收集技术所带来的额外操作只是与待回收的内存数量有关的效率要低。
- 同时,因为对象之间相互引用,每个对象的引用都不会为0,所以这些对象所占用的内存始终都不会被释放掉。
1.2 标记-清除
- 它分为两个阶段:第一阶段是标记阶段,GC会把所有的活动对象打上标记,第二阶段是把那些没有标记的对象非活动对象进行回收。
- 对象之间通过引用(指针)连在一起,构成一个有向图
- 从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。
- 根对象就是全局变量、调用栈、寄存器。
- 在上图中,可以从程序变量直接访问块1,并且可以间接访问块2和3,程序无法访问块4和5
- 第一步将标记块1,并记住块2和3以供稍后处理。
- 第二步将标记块2,第三步将标记块3,但不记得块2,因为它已被标记。
- 扫描阶段将忽略块1,2和3,因为它们已被标记,但会回收块4和5。
1.3 分代回收
- 分代回收是建立在标记清除技术基础之上的,是一种以空间换时间的操作方式。
- Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代)
- 他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。
- 新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发
- 把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推
- 老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。
02.Golang-三色标记法
2.1 三色标记法介绍
三色标记法只是为了叙述方便而抽象出来的一种说法,实际上的对象是没有三色之分的。
这里的三色,对应了垃圾回收过程中对象的三种状态:
- :对象已被标记,对应位为-- 该对象不会在本次 GC 中被回收 - 已被回收器访问到的对象,其中所有字段都已被扫描 - 黑色对象中任何一个指针都不可能直接指向白色对象
3)白色(要被清除的)
:对象未被标记,gcmarkBits
对应位为0
– 该对象将会在本次 GC 中被清理
2.2 具体流程如下图
- 就是标记内存中那些还在使用中(即被引用了)的部分
- 而内存中不再使用(即未被引用)的部分,就是要回收的垃圾,需要将其回收
- 上图中的 A、B、D 就是被引用正在使用的内存
- 而 C、F、E 曾经被使用过,但现在没有任何对象引用,就需要被回收掉。
- 而 Root 区域主要是程序运行到当前时刻的栈和全局数据区域,是实时正在使用到的内存,当然应该优先标记。
- 而考虑到内存块中存放的可能是指针,所以还需要递归的进行标记,待全部标记完后,就会对未被标记的内存进行回收。
2.3 STW弊端和优化
- STW弊端
- golang 的垃圾回收算法属于 标记-清除,是需要 STW 的
- STW 就是 Stop The World 的意思,在 golang 中就是要停掉所有的 goroutine,专心进行垃圾回收,待垃圾回收结束后再恢复 goroutine
- 而 STW 时间的长短直接影响了应用的执行,如果时间过长,那将是灾难性的。
- 为了缩短 STW 时间,golang 不对优化垃圾回收算法
- 其中写屏障(Write Barrier)和辅助 GC(Mutator Assist)就是两种优化垃圾回收的方法
1)写屏障(Write Barrier)
- 而写屏障就是让 goroutine 与 GC 同时运行的手段,虽然不能完全消除 STW,但是可以大大减少 STW 的时间。
- 写屏障在 GC 的特定时间开启,开启后指针传递时会把指针标记,即本轮不回收,下次 GC 时再确定。
2)辅助 GC(Mutator Assist)
- 为了防止内存分配过快,在 GC 执行过程中
- GC 过程中 mutator 线程会并发运行,而 mutator assist 机制会协助 GC 做一部分的工作
2.4 写屏障
1、STW解决的问题
标记过程需的要STW
,因为对象引用关系如果在标记阶段做了修改,会影响标记结果的正确性
。- 例如下图(假设没有STW)
- 1)灰色对象B引用白色对象C(此时C尚未被扫描)
- 2)当遍历完A对象后,A变成黑色
- 如果有其他程序断开了B对C的引用
- 同时添加了A对C的引用(由于A是黑色,所以C会一直是白色,被回收)
2、屏障技术
1)强三色不变式
:强三色不变式很好理解,强制性的不允许黑色对象引用白色对象即可插入屏障
:插入屏障拦截将白色指针插入黑色对象的操作,标记其对应对象为灰色状态
2)弱三色不变式
:所有被黑色对象引用的白色对象都处于灰色保护状态删除屏障
:也是拦截写操作的,但是是通过保护灰色对象到白色对象的路径不会断来实现的- 被灰色引用的对象,被删除了最后一个指向它的指针,也依旧可以活过这一轮,在下一轮GC中被清理掉
3、混合写屏障
插入屏障 优缺点
- 插入写屏障在标记开始时无需STW,可直接开始,并发进行
- 但结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活
删除屏障 优缺点
- 删除写屏障则需要在GC开始时STW扫描堆栈来记录初始快照
- 这个过程会保护开始时刻的所有存活对象,但结束时无需STW
Go1.8版本引入的混合写屏障
- 同样允许黑色对象引用白色对象,白色对象处于灰色保护状态,但是
只由堆上的灰色对象保护
。 - 只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW
- 而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。
- 同样允许黑色对象引用白色对象,白色对象处于灰色保护状态,但是
混合写屏障两种情况
- - 灰色对象B在栈上,引用堆上的白色对象C,将其引用关系删除,且新增一个黑色对象到对象C的引用 - 那么就需要通过shade(ptr)来保护了,在指针插入黑色对象时会触发对对象C的置灰操作。 - 如果栈已经被扫描过了,那么栈上引用的对象都是灰色或受灰色保护的白色对象了,所以就没有必要再进行这步操作。
2.5 垃圾回收触发机制
- 1、内存分配量达到阈值
- 每次内存分配都会检查当前内存分配量是否达到阈值,如果达到阈值则触发 GC。
阈值 = 上次 GC 内存分配量 * 内存增长率
- 内存增长率由环境变量
GOGC
控制,默认为 100,即每当内存扩大一倍时启动 GC。
- 2、定时触发 GC
- 默认情况下,2 分钟触发一次 GC,该间隔由
src/runtime/proc.go
中的forcegcperiod
声明。
- 默认情况下,2 分钟触发一次 GC,该间隔由
- 3、手动触发 GC
- 在代码中,可通过使用
runtime.GC()
手动触发 GC。
- 在代码中,可通过使用
2.6 GC 优化建议
- 由上文可知,GC 性能是与对象数量有关的,对象越多 GC 性能越差,对程序的影响也越大。
- 所以在开发中要尽量减少对象分配个数,采用对象复用、将小对象组合成大对象或采用小数据类型(如使用
int8
代替int
)等。
2.7 结语
- 一门编程语言的垃圾回收机制会直接影响使用其开发应用的性能。
- 在日常开发工作中也因注意到其作用,有助于开发出高性能的应用,这也是 GC 常常在面试中被问到的原因。
- 同时,了解 GC 对了解内存管理也很有帮助。