Python 垃圾回收机制

       一些高级语言,比如Java、C#都会有垃圾回收机制,防止一些没有的空间占用过多的内存,最后导致程序宕掉,c,c++里用户自己管理维护内存的方式,内存的申请、释放需要用户手动操作。Python底层也制定了垃圾回收机制,因此普通用户在使用Python时,不用关心何时开启、执行垃圾回收机制。

Python的垃圾回收机制主要采用的是引用计数为主、标记清除隔代回收为辅的垃圾回收策略。

小整数对象池

整数在程序的使用中会非常频繁,因此Python在程序运行之前,会在内存中创建出小整数对象池供用户使用,防止频繁操作整数对象进行内存的申请与销毁。

Python小整数对象池的范围是[-5, 256],这些整数对象都是提前创建好的,在这个范围内的整数独占内存,不会被垃圾回收。


小整数对象池举例1


小整数对象池举例2

超过这个范围,Python就会创建新的对象,开辟新的内存空间:

小整数对象池举例3

intern机制

a = "HelloPython"

b = "HelloPython"

c = "HelloPython"

类似这样的,Python去不会创建3个字符串对象,而是会默认开启intern机制,将这三个对象指向同一个内存空间,节省内存开销。

intern机制举例1

但是如果字符串中存在特殊字符,情况会有例外,intern机制就会失效:

intern机制举例2

引用计数机制

Python中,每一个东西都是对象,底层实现中都是一个PyObject:


其中,ob_refcnt属性记录着该对象的引用次数,当有东西引用该对象时,该属性会加1,引用该对象被删除时,该属性会减1,如果引用次数为0,就会触发垃圾回收机制

引用计数机制的优点:简单 实时性,一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。

引用计数机制的缺点:引用计数耗费资源;出现循环引用时,会出现问题


循环引用


图示引用计数缺点

t1与t2形成了循环引用,删除t1与t2之后,仍有引用计数,会导致内存空间不断被占用,进而导致宕机

为了解决这一问题,Python又引入了标记清除与隔代回收机制

标记清除机制

请注意在以上刚刚说到的例子中,我们以一个不是很常见的情况结尾:我们有一个“孤岛”或是一组未使用的、互相指向的对象,但是谁都没有外部引用。换句话说,我们的程序不再使用这些节点对象了,所以我们希望Python的垃圾回收机制能够足够智能去释放这些对象并回收它们占用的内存空间。但是这不可能,因为所有的引用计数都是1而不是0。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链表中即可。

那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?

对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。

隔代回收机制

Python使用一种不同的链表来持续追踪活跃的对象。而不将其称之为“活跃列表”,Python的内部C代码将其称为零代(Generation Zero)。每次当你创建一个对象或其他什么值的时候,Python会将其加入零代链表:


从上边可以看到当我们创建ABC节点的时候,Python将其加入零代链表。请注意到这并不是一个真正的列表,并不能直接在你的代码中访问,事实上这个链表是一个完全内部的Python运行时。 相似的,当我们创建DEF节点的时候,Python将其加入同样的链表:


现在零代包含了两个节点对象。(他还将包含Python创建的每个其他值,与一些Python自己使用的内部值。)

检测循环引用

随后,Python会循环遍历零代列表上的每个对象,检查列表中每个互相引用的对象,根据规则减掉其引用计数。在这个过程中,Python会一个接一个的统计内部引用的数量以防过早地释放对象。

为了便于理解,来看一个例子:


从上面可以看到 ABC 和 DEF 节点包含的引用数为1.有三个其他的对象同时存在于零代链表中,蓝色的箭头指示了有一些对象正在被零代链表之外的其他对象所引用。(接下来我们会看到,Python中同时存在另外两个分别被称为一代和二代的链表)。这些对象有着更高的引用计数因为它们正在被其他指针所指向着。

接下来你会看到Python的GC是如何处理零代链表的。



导致引用计数+1的情况

1、对象被创建,例如a=23 

2、对象被引用,例如b=a 

3、对象被作为参数,传入到一个函数中,例如func(a) 

4、对象作为一个元素,存储在容器中,例如list1=[a,a]

导致引用计数-1的情况

1、对象的别名被显式销毁,例如del a 

2、对象的别名被赋予新的对象,例如a=24 

3、一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会) 

4、对象所在的容器被销毁,或从容器中删除对象

GC常用函数:

1、gc.set_debug(flags)设置gc的debug日志,一般设置为gc.DEBUG_LEAK

2、gc.collect([generation])显式进行垃圾回收,可以输入参数,0代表只检查第一代的对象,1代表检查一,二代的对象,2代表检查一,二,三代的对象,如果不传参数,执行一个full collection,也就是等于传2。 返回不可达(unreachable objects)对象的数目

3、gc.get_threshold()获取的gc模块中自动执行垃圾回收的频率。

4、gc.set_threshold(threshold0[, threshold1[, threshold2])设置自动执行垃圾回收的频率。

5、gc.get_count()获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表

你可能感兴趣的:(Python 垃圾回收机制)