Python的垃圾回收梗概
If there’s no one left in the living world to remember you, you disappear from this world.
——Moive Coco
REFCNT
引用计数基于一个朴素的想法,一块内存,如果没有引用指向它,就不可达,不能再被访问,因此可以被释放回收。
有什么优势?
实时性。不用等到某个特殊的时刻,内存对象一旦引用为0就销毁。相对于一些集中进行垃圾检测、并回收的方案,引用计数把内存管理的花费分摊到运行时,程序更平稳一些。
有什么劣势?
- 额外的时间和空间的开销。伴随内存对象的创建,都需要维护的refcnt字段,并作相应的加减判断运算。一个有意思的场景是,对一些大量快速创建并自毁的对象来说,如果其生存周期处在非实时gc方案的两次收集之间,gc根本就不曾知道这些对象存在过,没有多余的开销;但对实时的引用计数,每个对象都得管理其引用计数。Python内部为了提高内存对象创建的效率,采用了建立对象池的方案。int、str、dict、list都建立相应的对象池。
- 不可达的循环引用。存在循环引用的对象,即使外部已经不存在对它的引用,无法访问到,它还是会因为引用计数大于0而无法回收。这是引用计数无解的死穴,必须通过其他方法来消除。
以整型42为例:
引用何处增加?
- 被赋值,a = 42,+1
- 添加新引用,b = a,+1
- 当作参数传入函数
is_anwser(candidate=a)
,+2(candidate一次新引用,frame的栈上一次) - 加入容器,
answers.apend(a)
,+1
引用何处减少?
- del语句,
del a
,显示切断名字a和内存对象42的引用联系 - 出离作用域。is_answer函数调用完成后,42的引用个数会减2。实际上,大部分内存回收都因为这个
- 从容器删除,
answers.pop()
分代标记-清除
分代的标记-清除,就是用来消除循环引用问题的。
因此,先明确分代标记-清除的定位,它是引用计数方案的补充,专门用于清除存在不可达的循环引用对象。
行为上,分代标记-清除,在特定时刻被调用,首先探测不可达的循环引用对象,然后将这些对象的refcnt强行置为0,引发引用计数方案的回收动作,从而释放内存
所以核心问题,怎么探测到不可达的循环引用对象?
标记-清除
想要构成循环引用,只能是容器,list、dict、instance、tuple之类的,所以python用双向链表来组织运行过程中建立的容器对象
如下图,黑色表示实际的引用关系,黄色双向箭头表示双向链表组织。希望标记出链表末尾的不可达循环引用对象,一个做法是,遍历元素,把元素的指向的子元素的refcnt减1,形成有效计数,因为如果有效计数还大于1,说明它是存在外部引用的可达对象,即root object,那么他们的子元素也是可达对象。
首先先做一个refcnt的备份,然后在备份上计算出有效计数
然后根据有效计数将链表分为reachable和unreachable两个链表。既然这里已经求出有效计数, 引用还大于0的,就是引用入口,最直观的做法就是从它开始dfs,设置标志,没有访问到的则是需要清理的不可达的对象。