Python垃圾回收机制,Python的废品回笼机制浓郁分

对于Python垃圾回收机制主要有三个,首先是使用引用计数来跟踪和回收垃圾,为了解决循环
引用问题,就采用标记-清除的方法,标记-清除的方法所带来的额外操作实际上与系统中总的内存
块的总数是相关的,当需要回收的内存块越多,垃圾检查带来的额外操作就越多,为了提高垃圾收集
的效率,采用“空间换时间的策略”,即使用分代机制,对于长时间没有被回收的内存就减少对它的
垃圾回收频率。

一、概述:

首先看一下Python的内存管理架构:

图片 1

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

layer 3: Object-specific memory(int/dict/list/string....)
Python 实现并维护
更高抽象层次的内存管理策略, 主要是各类特定对象的缓冲池机制

layer 2: Python's object allocator
Python 实现并维护
实现了创建/销毁Python对象的接口(PyObject_New/Del), 涉及对象参数/引用计数等

layer 1: Python's raw memory allocator (PyMem_ API)
Python 实现并维护, 包装了第0层的内存管理接口, 提供统一的raw memory管理接口
封装的原因: 不同操作系统行为不一定一致, 保证可移植性, 相同语义相同行为

layer 0: Underlying general-purpose allocator (ex: C library malloc)
操作系统提供的内存管理接口, 由操作系统实现并管理, Python不能干涉这一层的行为

Python 垃圾回收机制

二、引用计数

引用计数机制

引用计数是一种垃圾收集机制,而且也是一种最直观,最简单的垃圾回收技术 当一个对象的引用被创建或者复制时,对象的引用计数加1;
当一个对象的引用被销毁 对象的引用计数减1。如果对象的引用计数减少为0,那么就意味着对象已经不会被任何人使用,可以将其所占有的内存释放。
引用计数机制的优点:实时性,对于任何内存一旦没有指向它的引用,就会立即被回收(这里需要满足阈值才可以)
引用计数机制的缺点:引用计数机制所带来的维护引用计数的额外操作与Python运行中所运行的内存分配和释放,引用赋值的
次数是成正比的,为了与引用计数机制搭配,在内存的分配和释放上获得最高的效率,Python设计了大量的
内存池机制,减少运行期间malloc和free的操作。

>>> from sys import getrefcount
>>> a = [1,2,3]
>>> getrefcount(a)
2
>>> b =a
>>> getrefcount(a)
3
>>>

内存管理

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

标记-清除机制

引用计数机制有个致命的弱点,就是可能存在循环引用的问题:
一组对象的引用计数都不为0,然而这些对象实际上并没有被任何外部变量引用,它们之间只是相互引用,这意味这个不会
有人使用这组对象,应该回收这些对象所占的内存,然后由于互相引用的存在, 每个对象的引用计数都不为0,因此这些对象
所占用的内存永远不会被回收。
标记-清除机制就是为了解决循环引用的问题。首先只有container对象之间才会产生循环引用,所谓container对象即是内部
可持有对其他对象的引用的对象,比如list、dict、class等,而像PyIntObject、PyStringObject这些是绝不可能产生循环引用的
所以Python的垃圾回收机制运行时,只需要检查这些container对象,为了跟踪每个container,需要将这些对象组织到一个集合中。
Python采用了一个双向链表,所以的container对象在创建之后,就会被插入到这个链表中。这个链表也叫作可收集对象链表。

为了解决循环引用的问题,提出了有效引用计数的概念,即循环引用的两个对象引用计数不为0,实际上有效的引用计数为0
假设两个对象为A、B,我们从A出发,因为它有一个对B的引用,则将B的引用计数减1;然后顺着引用达到B,因为B有一个对A的引用,
同样将A的引用减1,这样,就完成了循环引用对象间环摘除。但是这样直接修改真实的引用计数,可能存在悬空引用的问题。
所以采用修改计数计数副本的方法。
这个计数副本的唯一作用是寻找root object集合(该集合中的对象是不能被回收的)。当成功寻找到root object集合之后,
我们就可以从root object出发,沿着引用链,一个接一个的标记不能回收的内存。首先将现在的内存链表一分为二,
一条链表中维护root object集合,成为root链表,而另外一条链表中维护剩下的对象,成为unreachable链表。之所以要剖成两个链表,
是基于这样的一种考虑:现在的unreachable可能存在被root链表中的对象,直接或间接引用的对象,这些对象是不能被回收的,
一旦在标记的过程中,发现这样的对象,就将其从unreachable链表中移到root链表中;当完成标记后,unreachable链表中剩下
的所有对象就是名副其实的垃圾对象了,接下来的垃圾回收只需限制在unreachable链表中即可。

Python中的内存管理机制的层次结构提供了4层,其中最底层则是C运行的malloc和free接口,往上的三层才是由Python实现并且维护的,第一层则是在第0层的基础之上对其提供的接口进行了统一的封装,因为每个系统都可能差异性。

原理:当一个对象的引用被创建或者复制时,对象的引用计数加1;当一个对象的引用被销毁时,对象的引用计数减1;当对象的引用计数减少为0时,就意味着对象已经没有被任何人使用了,可以将其所占用的内存释放了。
虽然引用计数必须在每次分配和释放内存的时候加入管理引用计数的动作,然而与其他主流的垃圾收集技术相比,引用计数有一个最大的有点,即 实时性 ,任何内存,一旦没有指向它的引用,就会立即被回收。而其他的垃圾收集计数必须在某种特殊条件下(比如内存分配失败)才能进行无效内存的回收。

分代回收

分代回收的思想:将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就称为一个“代”
垃圾收集的频率随着“代”的存活时间的增大而减小,也就是说,活的越长的对象,就越可能不是垃圾,就应该
越少去收集。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代中。
在Python中总共有三个“代”,每个代其实就是上文中所提到的一条可收集对象链表。下面的数组就是用于分代
垃圾收集的三个“代”。

#define NUM_GENERATIONS 3
#define GEN_HEAD(n) (&generations[n].head)

// 三代都放到这个数组中
/* linked lists of container objects */
static struct gc_generation generations[NUM_GENERATIONS] = {
/* PyGC_Head, threshold, count */
{{{GEN_HEAD(0), GEN_HEAD(0), 0}}, 700, 0}, //700个container, 超过立即触发垃圾回收机制
{{{GEN_HEAD(1), GEN_HEAD(1), 0}}, 10, 0}, // 10个
{{{GEN_HEAD(2), GEN_HEAD(2), 0}}, 10, 0}, // 10个
};

PyGC_Head *_PyGC_generation0 = GEN_HEAD(0);

其中存在三个阈值,分别是700,10,10
可以通过get_threshold()方法获得阈值:

import gc
print(gc.get_threshold())
(700, 10, 10) 

其中第一个阈值表示第0代链表最多可以容纳700个container对象,超过了这个极限值,就会立即出发垃圾回收机制。

后面两个阈值10是分代有关系,就是每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,
才会有1次的2代垃圾回收。也就是空间换时间的体现。*

垃圾回收的流程:
--> 分配内存的时候发现超过阈值(第0代的container个数),触发垃圾回收
--> 将所有可收集对象链表放在一起(将比当前处理的“代”更年轻的"代"的链表合并到当前”代“中)
--> 计算有效引用计数
--> 根据有效引用计数分为计数等于0和大于0两个集合
--> 引用计数大于0的对象,放入下一代
--> 引用计数等于0的对象,执行回收
--> 回收遍历容器内的各个元素, 减掉对应元素引用计数(破掉循环引用)
--> python底层内存管理机制回收内存

*

参考文档:



python源码剖析

图片 2 

引用计数机制执行效率问题:引用计数机制所带来的维护引用计数的额外操作与Python运行中所进行的内存分配和释放,引用赋值的次数是成正比的。而这点相比其他主流的垃圾回收机制,比如 标记-清除 , 停止-复制 ,是一个弱点,因为这些技术所带来的额外操作基本上只是与待回收的内存数量有关。
如果说执行效率还仅仅是引用计数机制的一个软肋的话,那么很不幸,引用计数机制还存在着一个致命的弱点,正是由于这个弱点,使得侠义的垃圾收集从来没有将引用计数包含在内,能引发出这个致命的弱点就是循环引用(也称交叉引用)。

内存池

问题说明:

Python为了避免频繁的申请和删除内存所造成系统切换于用户态和核心态的性能问题,从而引入了内存池机制,专门用来管理小内存的申请和释放。内存池分为四层:block、pool、arena和内存池。如下图:

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

图片 3

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

block:有很多种block,不同种类的block都有不同的内存大小,申请内存的时候只需要找到适合自身大小的block即可,当然申请的内存也是存在一个上限,如果超过这个上限,则退化到使用最底层的malloc进行申请。

这一点是致命的,这与手动进行内存管理所产生的内存泄露毫无区别。
要解决这个问题,Python引入了其他的垃圾收集机制来弥补引用计数的缺陷: 标记-清除 , 分代回收 两种收集技术。

pool:一个pool管理着一堆有固定大小的内存块,其大小通常为一个系统内存页的大小。

三、标记-清除

arena:多个pool组合成一个arena。

标记-清除 是为了解决循环引用的问题。可以包含其他对象引用的容器对象(比如: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链表中即可。

垃圾回收

四、分代回收

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

背景:分代的垃圾收集技术是在上个世纪80年代初发展起来的一种垃圾收集机制,一系列的研究表明:无论使用何种语言开发,无论开发的是何种类型,何种规模的程序,都存在这样一点相同之处。即:一定比例的内存块的生存周期都比较短,通常是几百万条机器指令的时间,而剩下的内存块,起生存周期比较长,甚至会从程序开始一直持续到程序结束。
从前面 标记-清除 这样的垃圾收集机制来看,这种垃圾收集机制所带来的额外操作实际上与系统中总的内存块的数量是相关的,当需要回收的内存块越多时,垃圾检测带来的额外操作就越多,而垃圾回收带来的额外操作就越少;反之,当需回收的内存块越少时,垃圾检测就将比垃圾回收带来更少的额外操作。为了提高垃圾收集的效率,采用 空间换时间的策略 。

引用计数

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

原理:当一个对象的引用被创建或者复制时,对象的引用计数加1;当一个对象的引用被销毁时,对象的引用计数减1,当对象的引用计数减少为0时,就意味着对象已经再没有被使用了,可以将其内存释放掉。

举例说明:

优点:引用计数有一个很大的优点,即实时性,任何内存,一旦没有指向它的引用,就会被立即回收,而其他的垃圾收集技术必须在某种特殊条件下才能进行无效内存的回收。

当某些内存块M经过了3次垃圾收集的清洗之后还存活时,我们就将内存块M划到一个集合A中去,而新分配的内存都划分到集合B中去。当垃圾收集开始工作时,大多数情况都只对集合B进行垃圾回收,而对集合A进行垃圾回收要隔相当长一段时间后才进行,这就使得垃圾收集机制需要处理的内存少了,效率自然就提高了。在这个过程中,集合B中的某些内存块由于存活时间长而会被转移到集合A中,当然,集合A中实际上也存在一些垃圾,这些垃圾的回收会因为这种分代的机制而被延迟。
在Python中,总共有3 代 ,也就是Python实际上维护了3条链表。具体可以查看Python源码详细了解。

缺点:但是它也有弱点,引用计数机制所带来的维护引用计数的额外操作与Python运行中所进行的内存分配和释放,引用赋值的次数是成正比的,这显然比其它那些垃圾收集技术所带来的额外操作只是与待回收的内存数量有关的效率要高。同时,引用技术还存在另外一个很大的问题-循环引用,因为对象之间相互引用,每个对象的引用都不会为0,所以这些对象所占用的内存始终都不会被释放掉。如下:

标记-清除

标记-清除的出现打破了循环引用,也就是它只关注那些可能会产生循环引用的对象,显然,像是PyIntObject、PyStringObject这些不可变对象是不可能产生循环引用的,因为它们内部不可能持有其它对象的引用。Python中的循环引用总是发生在container对象之间,也就是能够在内部持有其它对象的对象,比如list、dict、class等等。这也使得该方法带来的开销只依赖于container对象的的数量。

原理:将集合中对象的引用计数复制一份副本,这个计数副本的作用是寻找root object集合(该集合中的对象是不能被回收的)。当成功寻找到root object集合之后,首先将现在的内存链表一分为二,一条链表中维护root object集合,成为root链表,而另外一条链表中维护剩下的对象,成为unreachable链表。一旦在标记的过程中,发现现在的unreachable可能存在被root链表中直接或间接引用的对象,就将其从unreachable链表中移到root链表中;当完成标记后,unreachable链表中剩下的所有对象就是名副其实的垃圾对象了,接下来的垃圾回收只需限制在unreachable链表中即可。

缺点:该机制所带来的额外操作和需要回收的内存块成正比。

分代

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

【编辑推荐】

本文由星彩网app下载发布于计算机编程,转载请注明出处:Python垃圾回收机制,Python的废品回笼机制浓郁分

TAG标签: 星彩网app下载
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。