python垃圾回收(Garbage Collection)
Python的GC模块主要运用了“引用计数”(reference counting)来跟踪和回收垃圾。在引用计数的基础上,还可以通过“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用的问题。通过“分代回收”(generation collection)以空间换取时间来进一步提高垃圾回收的效率
1.引用计数(主要)
在Python中,大多数对象的生命周期都是通过对象的引用计数来管理的。从广义上来讲,引用计数也是一种垃圾收集机制,而且也是一种最直观,最简单的垃圾收集技术。
引用计数原理:当一个对象的引用被创建或者复制时,对象的引用计数加1;当一个对象的引用被销毁时,对象的引用计数减1;当对象的引用计数减少为0时,就意味着对象已经没有被任何人使用了,可以将其所占用的内存释放了。
优点:简单、实时性,任何内存,一旦没有指向它的引用,就会立即被回收。而其他的垃圾收集计数必须在某种特殊条件下(比如内存分配失败)才能进行无效内存的回收。
缺点:
维护性高,引用计数机制所带来的维护引用计数的额外操作与Python运行中所进行的内存分配和释放以及引用赋值的次数是成正比的。而这点相比其他主流的垃圾回收机制,比如“标记-清除”,“停止-复制”,是一个弱点,因为这些技术所带来的额外操作基本上只是与待回收的内存数量有关。
致命弱点:循环引用,循环引用可以使一组对象的引用计数不为0,然而这些对象实际上并没有被任何外部对象所引用,它们之间只是相互引用。这意味着不会再有人使用这组对象,应该回收这组对象所占用的内存空间,然后由于相互引用的存在,每一个对象的引用计数都不为0,因此这些对象所占用的内存永远不会被释放。比如:
a = [1,2]
b = [3,4]
a.append(b)
b.append(a)
print(a)
[1, 2, [3, 4, [...]]]
print(b)
[3, 4, [1, 2, [...]]]
del b
del a
如上代码a、b间的引用都为1,而a、b被引用的对象删除后都各自减去1,所以他们各自的引用计数还是1。这样的情况单单靠引用计数就无法解决了,Python引入了其他的垃圾收集机制来弥补引用计数的缺陷:“标记-清除”,“分代回收”两种收集技术。
2.标记-清除
“标记-清除”是为了解决循环引用的问题。可以包含其他对象引用的容器对象(比如:list,set,dict,class,tuple)都可能产生循环引用。为了追踪容器对象,需要每个容器对象维护两个额外的指针, 用来将容器对象组成一个链表,指针分别指向前后两个容器对象,方便插入和删除操作。
试想一下,现在有两种情况:
第一种情况:
a=[1,2] b=[3,4] a.append(b) b.append(a) del a del b
第二种情况:
a=[1,2] b=[3,4] a.append(b) b.append(a) del a
在标记-清除算法中,有两个链表,一个是root链表(root object)(不回收),另外一个是unreachable链表(回收)。
对于第一种情况,在未执行del语句的时候,a,b的引用计数都为2(init(初始化)+append(引用)=2),但是在del执行完以后,a,b引用次数互相减1,此时a,b的引用计数都为1,如果两个对象的引用计数都为1,但是仅仅存在他们之间的循环引用,那么这两个对象都是需要被回收的,也就是说,它们的引用计数虽然表现为非0,但实际上有效的引用计数为0。此时就需要标记-清除垃圾回收机制介入,我们必须先将循环引用摘掉,那么这两个对象的有效计数就现身了。假设两个对象为A、B,我们从A出发,因为它有引用B,当A被回收时,则将B的引用计数减1;然后顺着引用达到B,因为它有引用A,当B被回收时,同样将A的引用减1,这样,就完成了循环引用对象间引用环摘除。引用环摘除之后发现,a,b循环引用变为了0,所以a,b就被处理到unreachable链表中直接被做掉。
对于第二种情况,进行循环引用摘除之前,因为只del a,所以a的引用计数减1为1,而b不变还是为2。进行循环引用摘除之后,b的引用计数为1,但是a就为0了。这个时候a已经进入unreachable链表中,等待被当作垃圾回收,但是这个时候,root链表中有b,b有引用a,如果a被清理了,b就有问题了。所以在循环引用摘除之后,还会继续检测现在的unreachable链表种是否存在被root链表中的对象直接或间接引用的对象,此时检测发现unreachable链表中的a有被root链表中的b引用,所以a又被拉到了root链表中,不被回收。
除了上面两种情况之外还存在一个问题,假设对象A有引用C,而C没有引用A,当回收A之前,如果将C计数引用减1,而到最后A其实并没有被回收,显然,我们错误的将C的引用计数减1,这将导致在未来的某个时刻出现一个对C的悬空引用。这就要求我们必须在A没有被删除的情况下复原C的引用计数,即在A没有被删除的之前将C的引用计数保存一份,如果采用这样的方案,那么维护引用计数的复杂度将成倍增加。
标记-清除原理:“标记-清除”采用了更好的做法,我们并不改动真实的引用计数,而是将集合中对象的引用计数复制一份副本,改动该对象引用的副本。对于副本做任何的改动,都不会影响到对象生命周期的维护。
这个计数副本的唯一作用是寻找root object集合(该集合中的对象是不能被回收的)。当成功寻找到root object集合之后,首先将现在的内存链表一分为二,一条链表中维护root object集合,成为root链表,而另外一条链表中维护剩下的对象,成为unreachable链表。之所以要剖成两个链表,是基于这样的一种考虑:现在的unreachable可能存在被root链表中的对象直接或间接引用的对象,这些对象是不能被回收的,一旦在标记的过程中,发现这样的对象,就将其从unreachable链表中移到root链表中;当完成标记后,unreachable链表中剩下的所有对象就是名副其实的垃圾对象了,接下来的垃圾回收只需限制在unreachable链表中即可,这也是为什么要搞这两个链表的原因。
3.分代回收
背景:分代的垃圾回收技术是在上个世纪80年代初发展起来的一种垃圾收集机制,一系列的研究表明:无论使用何种语言开发,无论开发的是何种类型,何种规模的程序,都存在这样一点相同之处。即:一定比例的内存块的生存周期都比较短,通常是几百万条机器指令的时间,而剩下的内存块,起生存周期比较长,甚至会从程序开始一直持续到程序结束。
从前面“标记-清除”这样的垃圾收集机制来看,这种垃圾收集机制所带来的额外操作实际上与系统中总的内存块的数量是相关的,当需要回收的内存块越多时,垃圾检测带来的额外操作就越多,而垃圾回收带来的额外操作就越少;反之,当需回收的内存块越少时,垃圾检测就将比垃圾回收带来更少的额外操作。为了提高垃圾收集的效率,采用“空间换时间的策略”。
分代回收原理:将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就称为一个“代”,垃圾收集的频率随着“代”的存活时间的增大而减小。也就是说,活得越长的对象,就越不可能是垃圾,就应该减少对它的垃圾收集频率。那么如何来衡量这个存活时间:通常是利用几次垃圾收集动作来衡量,如果一个对象经过的垃圾收集次数越多,可以得出:该对象存活时间就越长。
GC的阈值,所谓阈值就是一个临界点的值。随着你的程序运行,Python解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,创建的数量应该等于释放数量。但是如果存在循环引用的话,肯定是创建数量大于释放数量,当创建数与释放数量的差值达到规定的阈值的时候,就需要分代回收机制发挥作用了。
参考:https://juejin.im/post/5b34b117f265da59a50b2fbe

浙公网安备 33010602011771号