现代语言基本都具备自己的垃圾回收机制, 自动的垃圾回收能减少程序员的负担和增加程序的可靠性.这次稍微深入了解一下go和python在垃圾回收
Python垃圾回收
引用计数+标记清除+分代回收
引用计数
每个对象都有一个变量指明有多少对象引用了它,当引用计数为0时,就直接将对象清除,释放内存
优点: 简单, 不需要集中处理
缺点: 无法处理循环引用的情况
标记-清除
目的:解决循环引用的对象无法被引用计数正常清理的问题.
工具: 两个链表,一个Object_to_Scan链表, 一个Unreachable链表
步骤:
- 遍历Object_to_Scan所有对象,将其引用计数-1,如果为0的话则放入Unreachable.
- 对存在于Object_to_Scan的对象,将从该对象可达的所有对象都标记为Reachable,如果被标记的对象在Unreachable链表中,则将其移动到Object_to_Scan链表.
- 释放Unreachable链表中的对象
注意: 标记-清除过程是阻塞的.会暂停整个应用程序
分代回收
目的:为何时进行标记-清除提供一个高效率的策略
做法: 所有对象都会处于0,1,2三个世代中, 新生对象处于0世代中.如果对象在标记-清除过程下存活,则将其置入下一代.世代越高,触发标记-清除的频率越低.
原理: 基于存活时间更久的更会被使用的想法.有点类似局部性原理.
QA
Q:合适触发标记-清除
A:第0代基于分配对象数量和释放对象数量的差值, 第1代和第2代基于上一代执行过的次数. 比如第一代的垃圾回收会在进行过10次0代垃圾回收后进行. 可以通过调用gc.get_threshold()
来获取默认配置.
Go垃圾回收
相对于python, Go虽然也使用了基于标记清除的三色标记法,但是Go的垃圾回收通过一些特殊机制来实现了不需要完全停止用户程序(STW, Stop The World)
三色标记算法
所有对象会有三个状态:
- 白色-非活跃
- 灰色-活跃(中间形态)
- 黑色-活跃
过程:
- 所有对象一开始都是白色,从root对象开始,将其引用的对象置为灰色
- 从灰色对象开始,将其置为黑色,并将其指向的所有对象置为灰色
- 重复步骤2,直到没有灰色对象.此时可以将所有存在的白色对象回收
可以结合下面的动图理解
如果只是这样,那就必须保证在进行标记时,用户程序不在进行,否则修改了对象之间的引用,则会引发错误的结果.因此引入了屏障技术来保证不中断用户程序的情况下能够正确的标记对象.
屏障技术
有读屏障和写屏障,实现读屏障或写屏障都需要更改因为读的频率更高,因此go使用的是写屏障.
要保证用户更改了对象的引用后,最终的回收过程能够正确,需要达成以下两种三色不变性(Tri-color invariant)中的一种:
- 强三色不变性 — 黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象;
- 弱三色不变性 — 黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径
如何理解三色不变性: 如果垃圾回收不与用户进程并行,黑色对象永远不会指向白色对象,只有在不符合预期的时候,黑色对象会指向白色对象.因此当黑色指向白色对象时,为了保证白色对象不被删除,就需要将白色对象最终置为黑色.实现强或弱三色不变性就能保证白色对象在清除阶段为黑色来保证不被错误的回收.
Dijkstra 插入写屏障
当有用户程序使黑色对象指向白色对象时,将白色对象置为灰色
删除写屏障
将灰色对象对白色对象的引用删除时,将白色对象置为灰色
合适进行标记清除
- 超过内存大小阈值. 阈值是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发。比如一次回收完毕后,内存的使用量为5M,那么下次回收的时机则是内存分配达到10M的时候。也就是说,并不是内存分配越多,垃圾回收频率越高。 如果一直达不到内存大小的阈值呢?这个时候GC就会被定时时间触发.
- 达到定时时间. 如果一直达不到阈值,那就定时(默认2min触发一次)触发一次GC保证资源的回收。
QA
Q: 既然用户程序和GC可以交替进行,如果用户程序分配对象比GC更快怎么办?
A: Go会有检测的机制,如果出现回收速度<分配速度,则会暂停用户程序,加快回收