Python垃圾回收机制

时间:2022-08-10 00:00:53
Python GC主要运用了“引用计数”(reference counting)来跟踪和回收垃圾。

在引用计数的基础上,通过“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用的问题。通过“分代回收”(generation collection)以空间换取时间来提高垃圾回收效率。

引用计数
在Python中,大多数对象的生命周期都是通过对象的引用计数来管理的。从广义上来讲,引用计数也是一种垃圾收集机制。并且也是一种最直观。最简单的垃圾收集技术。
原理:当一个对象的引用被创建或者复制时。对象的引用计数加1。当一个对象的引用被销毁时,对象的引用计数减1;当对象的引用计数降低为0时,就意味着对象已经没有被不论什么人使用了。能够将其所占用的内存释放了。
尽管引用计数必须在每次分配和释放内存的时候增加管理引用计数的动作,然而与其它主流的垃圾收集技术相比。引用计数有一个最大的有点,即“实时性”。不论什么内存,一旦没有指向它的引用,就会马上被回收。而其它的垃圾收集计数必须在某种特殊条件下(比方内存分配失败)才干进行无效内存的回收。
引用计数机制执行效率问题:引用计数机制所带来的维护引用计数的额外操作与Python执行中所进行的内存分配和释放。引用赋值的次数是成正比的。而这点相比其它主流的垃圾回收机制,比方“标记-清除”,“停止-复制”,是一个弱点,由于这些技术所带来的额外操作基本上仅仅是与待回收的内存数量有关。


假设说运行效率还不过引用计数机制的一个软肋的话,那么非常不幸。引用计数机制还存在着一个致命的弱点,正是因为这个弱点,使得侠义的垃圾收集从来没有将引用计数包括在内,能引发出这个致命的弱点就是循环引用(也称交叉引用)。
问题:
循环引用能够使一组对象的引用计数不为0。然而这些对象实际上并没有被不论什么外部对象所引用。它们之间仅仅是相互引用。这意味着不会再有人使用这组对象。应该回收这组对象所占用的内存空间,然后因为相互引用的存在,每个对象的引用计数都不为0。因此这些对象所占用的内存永远不会被释放。比方:
a = []
b = []
a.append(b)
b.append(a)
print a
[[[…]]]
print b
[[[…]]]
这一点是致命的。这与手动进行内存管理所产生的内存泄露毫无差别。
要解决问题。Python引入了其它的垃圾收集机制来弥补引用计数的缺陷:“标记-清除”。“分代回收”两种收集技术。

标记-清除
“标记-清除”是为了解决循环引用的问题。能够包括其它对象引用的容器对象(比方:list,set,dict。class。instance)都可能产生循环引用。
我们必须承认一个事实。如果两个对象的引用计数都为1。可是只存在他们之间的循环引用,那么这两个对象都是须要被回收的。也就是说。它们的引用计数尽管表现为非0。但实际上有效的引用计数为0。我们必须先将循环引用摘掉,那么这两个对象的有效计数就现身了。如果两个对象为A、B。我们从A出发,由于它有一个对B的引用,则将B的引用计数减1;然后顺着引用达到B,由于B有一个对A的引用,相同将A的引用减1,这样,就完毕了循环引用对象间环摘除。
可是这样就有一个问题,假设对象A有一个对象引用C,而C没有引用A。假设将C计数引用减1,而最后A并没有被回收,显然。我们错误的将C的引用计数减1,这将导致在未来的某个时刻出现一个对C的悬空引用。这就要求我们必须在A没有被删除的情况下复原C的引用计数,假设採用这种方案。那么维护引用计数的复杂度将成倍添加。


原理:“标记-清除”採用了更好的做法,我们并不修改真实的引用计数,而是将集合中对象的引用计数复制一份副本。修改该对象引用的副本。

对于副本做不论什么的修改,都不会影响到对象生命走起的维护。
这个计数副本的唯一作用是寻找root object集合(该集合中的对象是不能被回收的)。

当成功寻找到root object集合之后。首先将如今的内存链表一分为二,一条链表中维护root object集合。成为root链表,而另外一条链表中维护剩下的对象,成为unreachable链表。之所以要剖成两个链表,是基于这种一种考虑:如今的unreachable可能存在被root链表中的对象。直接或间接引用的对象,这些对象是不能被回收的,一旦在标记的过程中。发现这种对象,就将其从unreachable链表中移到root链表中。当完毕标记后。unreachable链表中剩下的全部对象就是名副事实上的垃圾对象了,接下来的垃圾回收仅仅需限制在unreachable链表中就可以。

分代回收
背景:分代的垃圾收集技术是在上个世纪80年代初发展起来的一种垃圾收集机制,一系列的研究表明:不管使用何种语言开发。不管开发的是何种类型,何种规模的程序,都存在这样一点同样之处。

即:一定比例的内存块的生存周期都比較短,一般是几百万条机器指令的时间。而剩下的内存块。起生存周期比較长。甚至会从程序開始一直持续到程序结束。


从前面“标记-清除”这种垃圾收集机制来看。这种垃圾收集机制所带来的额外操作实际上与系统中总的内存块的数量是相关的。当须要回收的内存块越多时,垃圾检測带来的额外操作就越多,而垃圾回收带来的额外操作就越少;反之。当需回收的内存块越少时,垃圾检測就将比垃圾回收带来更少的额外操作。为了提高垃圾收集的效率。採用“空间换时间的策略”。
原理:将系统中的全部内存块依据其存活时间划分为不同的集合,每个集合就成为一个“代”,垃圾收集的频率随着“代”的存活时间的增大而减小。

也就是说,活得越长的对象。就越不可能是垃圾,就应该降低对它的垃圾收集频率。那么怎样来衡量这个存活时间:一般是利用几次垃圾收集动作来衡量,假设一个对象经过的垃圾收集次数越多,能够得出:该对象存活时间就越长。



举例:
当某些内存块M经过了3次垃圾收集的清洗之后还存活时,我们就将内存块M划到一个集合A中去。而新分配的内存都划分到集合B中去。当垃圾收集開始工作时,大多数情况都仅仅对集合B进行垃圾回收,而对集合A进行垃圾回收要隔相当长一段时间后才进行,这就使得垃圾收集机制须要处理的内存少了,效率自然就提高了。

在这个过程中,集合B中的某些内存块由于存活时间长而会被转移到集合A中,当然,集合A中实际上也存在一些垃圾,这些垃圾的回收会由于这样的分代的机制而被延迟。


在Python中。总共同拥有3“代”,也就是Python实际上维护了3条链表。

具体能够查看Python源代码具体了解。