zoukankan      html  css  js  c++  java
  • 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引入了分代回收的机制,以空间换时间。

  • 相关阅读:
    周末现场支持
    变量&字符串
    dead loop、continue & break、while...else语句
    运算符、流程控制、while循环
    二进制、字符编码、浮点数、列表
    字符串操作
    元祖、hash了解、字典、集合
    大数据处理
    含有虚函数的派生类的sizeof
    eclipse UML插件 安装和使用
  • 原文地址:https://www.cnblogs.com/huangjiangyong/p/14109072.html
Copyright © 2011-2022 走看看