Python中的垃圾回收机制

  垃圾回收(Garbage Collection,以下简称GC)是一种自动的内存管理机制,有许多不同的实现算法,Python中的GC,以引用计数为主,标记-清除和分代回收为辅。


1、GC

  在程序中定义了一个变量,就是在内存中开辟了一段相应的空间来存值。由于内存是有限的,所以当程序不再需要使用某个变量的时候,就需要销毁该对象并释放其所占用的内存资源,好重新利用这段空间。在C/C++中,无用变量的内存空间,需要由程序员手动释放,这显然非常繁琐,而且一旦有所疏忽,就可能造成内存泄漏(Memory Leak)。当软件系统比较复杂、变量多的时候,程序员往往就忘记释放内存或者在不该释放的时候释放了内存。
  有了GC,程序员就不需要再手动地去控制内存的释放,这一切可以交由语言本身来自动完成。GC本质上做了三件事情:1).为新生对象分配内存;2).垃圾检测;3).垃圾回收。GC是一门古老的艺术,大概在1960年就有了,发展至今,已经有相当多的算法,如标记-清除、引用计数、标记-压缩、分代回收等。GC的来源可能是由编程语言本身内置(如Java、C#)或是经由外面的库所提供,而非建制于语言内部,例如贝姆垃圾收集器就是一种可支持C/C++语言的自动内存管理工具。
  GC最早起源于LISP语言,目前许多语言如Java、C#、Ruby、Python等都支持GC(值得注意的是,C++自身并不支持GC,读者可参考12)。接下来我们就详细地讲解一下Python中的GC。

2、Python中的GC

  Python中的GC,以引用计数为主,标记-清除和分代回收为辅。

2.1、引用计数(reference counting)

  引用计数,是George E. Collins在1960年发明的,算是最早期的垃圾回收实现方法。
  在Python中,每一个对象的核心就是一个结构体PyObject,它的内部有一个引用计数器ob_refcnt

 typedef struct_object {
 int ob_refcnt;
 struct_typeobject *ob_type;
} PyObject;

当一个对象有新的引用时,对象的引用计数+1;当一个对象的引用被销毁时,对象的引用计数-1;当对象的引用计数减少为0时,就意味着对象已经没有被任何人使用了,可以将其所占用的内存释放。

  导致引用计数+1的情况:1).对象被创建;2).对象被复制;3).对象作为函数参数被传入(引用计数+2);4).对象作为一个元素被存储在容器中.
  导致引用计数-1的情况:1).对象的别名被显式销毁;2).对象的别名被赋予新的对象;3).一个对象离开它的作用域,例如函数执行完毕后,其中的局部变量;4).对象所在的容器被销毁,或从容器中删除对象。

  引用计数机制的优缺点是显而易见的:

优点:

  1. 简单;
  2. 实时性:一旦引用计数为0,立即被回收;内存回收的时间分摊到平时;

缺点:

  1. 需要额外的空间来维护引用计数;
  2. 执行效率低:引用计数机制所带来的维护引用计数的额外操作,与程序运行过程中所进行的内存分配、释放和引用赋值的次数成正比

  除了上面提到的,引用计数机制还有一个致命缺点,即无法解决循环引用的问题。我们用一段代码来做进一步的解释:

a = [1, 2] # 对象[1, 2]的引用计数为1
b = [3, 4] # 对象[3, 4]的引用计数为1
a.append(b) # 对象[3, 4]的引用计数为2
b.append(a) # 对象[1, 2]的引用计数为2
del a # 对象[1, 2]的引用计数为1
del b # 对象[3, 4]的引用计数为1

上面的代码中,对象[1, 2]和[3, 4]已经没有了来自外界的引用,这意味着不会再有人使用它们(无法通过其它变量来引用这两个对象),但是它们彼此之间依然有相互的引用,因此引用计数均为1,也就导致它们的内存永远不能被回收。
  这一点是致命的,它与手动进行内存管理所产生的内存泄漏无异(因此,也有很多语言比如Java并没有采用引用计数来实现GC)。为了弥补引用计数的缺陷,Python中引入了其它的GC机制。

2.2、标记-清除(mark and sweep)

  可以包含其它对象引用的容器对象,如list、set、dict、class、instance,都可能产生循环引用,标记-清除可以解决这个问题。
  标记-清除是一种基于追踪(Tracing)回收技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记,把所有的『活动对象』打上标记,第二阶段是回收,对那些没有标记的『非活动对象』进行回收。那么,如何区分活动对象和非活动对象呢?
  对象之间会通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从root object出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达(unreachable)的对象就是要被清除的非活动对象。所谓root object,就是一些全局变量、调用栈、寄存器,这些对象是不可被删除的。

在这里插入图片描述
在上图中,我们把小黑圈视为root object,从小黑圈出发,对象1可达,那么它将被标记,对象2、3可间接可达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。
  标记-清除的过程实际比上面说的还要复杂一下,具体来讲,首先找到root object集合,然后在内存中建立两条链表,一条链表中维护root object集合,称为root链表,而另外一条链表中维护剩下的对象,称为unreachable链表。在标记的过程中,如果发现unreachable链表中存在被root链表中的对象,直接或间接引用的对象,就将其从unreachable链表中移到root链表中;当完成标记后,unreachable链表中剩下的所有对象就是名副其实的垃圾对象了,接下来的垃圾回收只需限制在unreachable链表中即可。

2.3、分代回收(generation collection)

  分代回收,是一种以空间换时间的回收方式,可以提升GC的效率。
  分代回收思想将对象分在不同的集合中,每个集合称为一个“代”(generation),Python中分为3代,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表。每一代的GC频率是不同的,第0代最高,第1代次之,第2代最低。
  根据弱代假说(即越年轻的对象越容易死掉,而老的对象通常会存活更久),新生的对象被放入第0代,如果该对象在第0代的一次GC中活了下来,那么它就被移动到第1代,类似地,如果某第1代对象在第1代的一次GC中活了下来,它就被移动到第2代。
  那么,什么情况下会触发GC呢?具体地,在Python中,gc.set_threshold(threshold0[,threshold1[,threshold2]])可以设置每一代GC被触发的阈值:从上一次第0代GC后,如果分配对象的个数减去释放对象的个数大于threshold0,那么就会对第0代中的对象进行GC; 从上一次第1代GC后,如果第0代被GC的次数大于threshold1,那么就会对第1代中的对象进行GC;同样,从上一次第2代GC后,如果第1代被GC的次数大于threshold2,那么就会对第2代中的对象进行GC。除此之外,还有两种情况会触发GC,第一种是手动调用gc.collect(),第二种便是程序退出。
  从上面的叙述可以看出,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。

2.4、其它

a).
  Python中的gc模块提供了一些接口给开发者设置GC相关的选项,具体使用可参考12
b).
  如果循环引用中,两个对象都定义了__del__方法,gc模块不会销毁这两个不可达对象,因为gc模块不知道应该先调用哪个对象的__del__方法(例如,两个对象a和b,如果先销毁a,则在销毁b时,会调用b的__del__方法,该方法中很可能使用了a,这时会造成异常),所以为了安全起见,gc模块会把对象放到gc.garbage中,并把它们称为uncollectable。很明显,这种情况会造成内存泄漏,要解决的话,只能显式调用其中某个对象的__del__方法来打破僵局。
c).
  还有一种情况会造成Python中的内存泄漏,即对象一直被全局变量所引用,而我们知道,全局变量的生命周期是非常长的。

2.5、小结

  写到这里,我们尝试着来小结一下Python中的GC机制:Python中,对于所有对象,引用计数都在起作用,一旦某对象的引用计数为0,它所占用的内存就会被释放;而对于容器对象,由于它们会产生循环引用,这是引用计数所无法解决的,于是Python引入了标记-清除的方式来对它们做GC;最后,为了提升标记-清除的GC效率,Python引入了分代回收的机制,以空间换时间。


参考文献

[1] https://juejin.im/post/5b34b117f265da59a50b2fbe
[2] https://www.cnblogs.com/pinganzi/p/6646742.html
[3] https://blog.csdn.net/bluehawksky/article/details/50295089
[4] https://www.cnblogs.com/Xjng/p/5128269.html
[5] https://toutiao.io/posts/206426/app_preview
[6] https://zhuanlan.zhihu.com/p/31150408
[7] https://blog.csdn.net/yueguanghaidao/article/details/11274737
[8] http://python.jobbole.com/83548/?utm_source=blog.jobbole.com&utm_medium=relatedPosts
[9] http://python.jobbole.com/87843/?utm_source=blog.jobbole.com&utm_medium=relatedPosts
[10] http://www.memorymanagement.org/mmref/recycle.html#tracing-collectors
[11] https://zh.wikipedia.org/wiki/垃圾回收_(計算機科學)
[12] http://blog.jobbole.com/109833/
[13] https://baike.baidu.com/item/GC/66426?fr=aladdin
[14] https://baike.baidu.com/item/内存泄漏/6181425?fromtitle=内存泄露&fromid=305116
[15] https://www.jianshu.com/p/b309f4cb579d
[16] https://blog.csdn.net/yeahhook/article/details/6796242
[17] https://blog.csdn.net/jx232515/article/details/52749551?utm_source=blogxgwz0
[18] https://www.zhihu.com/question/33529443
以上为本文的全部参考文献,对原作者表示感谢。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值