【Python垃圾回收机制】 -- 2019-08-09 10:26:20

2019-08-09

【Python垃圾回收机制】 -- 2019-08-09 10:26:20

原文: http://106.13.73.98/__/186/

Python的GC模块主要运用了 引用计数 (reference counting)来跟踪和回收垃圾。在引用计数的基础上,还可以通过 标记-清除(mark and sweep)解决容器对象可能产生的循环引用问题。通过 分代回收(generation collection)以空间换取时间来进一步提高垃圾回收的效率。

@
___

引用计数

在Python中,大多数对象的生命周期都是通过对象的引用计数来管理的。从广义上来讲,引用计数也是一种垃圾收集机制,而且也是一种最直观,最简单的垃圾收集计数。

引用计数的原理:

  1. 当一个对象的引用被创建或复制时,对象的引用计数加1;
  2. 当一个对象的引用被销毁时,对象的引用计数减1;
  3. 当一个对象的引用计数为0时,就意味着该对象不再被使用了,将释放其占用的内存。

    这里有一个类A,且看下面的操作:

    class A:
        def __init__(self):
            print('初始化完成')
    
        def __del__(self):
            print('使命完成,我去也')
    
    
    a1 = A()
    a2 = a1
    a3 = a1
    
    
    del a1
    print('删除a1')
    
    del a2
    print('删除a2')
    
    del a3
    print('删除a3')
    
    
    print('代码执行结束')
    
    
    """
    打印顺序为:
        初始化完成
        删除a1
        删除a2
        使命完成,我去也
        删除a3
        代码执行结束
    """

    ????????首先,我们创建了一个A类的对象:a1,然后通过执行赋值语句定义了a2a3,此时它们共同指向一块内存地址,专业的解释是:A类对象的引用计数为3。紧接着执行del关键字,删除对象的一个引用,当引用为0时,对象才被撤销,这就是Python垃圾回收机制之引用计数。

引用计数最大的一个优点——实时性:
????????虽然引用计数必须在每次分配和释放内存的时候加入管理引用计数的动作,然而与其它主流的垃圾收集计数相比,引用计数有一个最大的优点,即 “实时性”。任何内存,一旦没有指向它的引用,就会立即被回收。而其它的垃圾收集机制必须在满足某种特殊的条件下(必须内存分配失败)才能进行无效内存的回收。

引用计数的执行效率问题:
????????引用计数机制带来的维护引用计数的额外操作与Python运行中所进行的内存分配、释放和引用赋值的次数是成正比的。而这点相比其它主流的垃圾回收机制,比如 “标记-清除”、“停止-复制”,是一个弱点。因为这些技术所带来的额外操作基本上只是与待回收的内存数量有关。

引用计数的致命弱点——循环引用:
????????如果说执行效率还仅仅是引用计数机制的一个软肋的话,那么根不幸,引用计数机制还存在一个致命的弱点,正是因为这个弱点,使得狭义的垃圾收集从来没有将引用计数包含在内,能引发出这个致命弱点的就是循环引用(也成交叉引用)。

所谓循环引用:

????????循环引用可以使一组对象的引用计数不为0,然而这些对象实际上并没有被任何外部对象所引用,它们之间只是相互引用。这意味着不会再有人使用这组对象,应该回收这组对象所占用的内存空间,然后由于相互引用的存在,每一个对象的引用计数都不为0,因此这些对象所占用的内存永远不会被释放。

>>> a = []
>>> b = [a]
>>> a.append(b)
>>> print(a)
[[[...]]]

这一点是致命的,这与手动进行内存管理所产生的内存泄漏毫无区别。要解决这个问题,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年代初发展起来的一种垃圾收集机制,一系列的研究表明:无论使用何种语言开发,无论开发的是何种类型,任何规模的程序,都存在这样一点相同之处。即:一定比例的内存块的声明周期都比较短,通常是几百万条机器指令的时间,而剩下的内存块,生存周期比较长,甚至会从程序开始一直持续到程序结束。

????????从前面 “标记-清除” 这样的垃圾收集机制来看,这种垃圾收集机制带来的额外操作实际上与系统中总的内存块的数量是相关的,当需要回收的内存块数量越多时,垃圾检测带来的额外操作就越多,而垃圾回收带来的额外操作就越少;反之,当需回收的内存块越少时,垃圾检测就比垃圾回收带来更多的额外操纵。为了提高垃圾收集的效率,采用 空间换时间的策略

分代回收原理:
????????将系统中所有内存块根据其存活时间划分为不同的集合,每一个集合就成为一个 “代”,垃圾收集的频率随着 “代” 的存活时间的增大而减小。也就是说,活的时间越长的对象,就越不可能是垃圾,就应该减少对它的垃圾收集频率。那么如何来衡量这个存活时间呢,通常是利用几次垃圾收集动作来衡量,如果一个对象经过的垃圾收集次数越多,可以得出:该对象存活时间就越长。

原文: http://106.13.73.98/__/186/

【Python垃圾回收机制】 -- 2019-08-09 10:26:20

【Python垃圾回收机制】 -- 2019-08-09 10:26:20

原文地址:https://www.cnblogs.com/gqy02/p/11325548.html