Python内存管理机制

Posted Winter Ding - 丁丁小爸爸

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python内存管理机制相关的知识,希望对你有一定的参考价值。

俗话说,出来混早晚要还的,Python还有很多知识点没有总结成博客,欠了太多,先还一部分吧

1. Python对象的内存使用

内存管理是语言设计的一个重要方面。它是决定语言性能的重要因素。无论是C语言的手工管理,还是Java的垃圾回收,都成为语言最重要的特征。

Python认为一切都是对象,在使用对象时需要进行内存管理,简单说,使用对象时需要借用系统资源,为对象分配内存;用完以后,同样需要释放借用的系统资源(防止内存泄露,当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。);对于python程序员来说,python的解释器承担了内存管理的复杂任务,所以python程序员刻意不必关心内存管理的问题;但是,了解一下Python的内存管理机制还是很有必要的;

1)引用计数机制

Python采用引用计数机制对内存进行管理;

python认为一切都是对象,它们的核心就是一个结构体:PyObject

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

PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少

#define Py_INCREF(op)   ((op)->ob_refcnt++) //增加计数
#define Py_DECREF(op) \\ //减少计数
    if (--(op)->ob_refcnt != 0) \\
        ; \\
    else \\
        __Py_Dealloc((PyObject *)(op))

当引用计数为0时,该对象生命就结束了。

以常见的赋值语句为例:

a = \'hello\'

当python解释器执行到这条语句,首先会创建一个字符串对象\'hello\',然后将该对象的引用赋值给a;在python中,存在一个内部跟踪变量,用于记录所有使用中的对象各有多少个引用,这个变量称为“引用计数”;

通过sys包中的getrefcount(),可以查看某个对象的引用计数;

 1 >>> import sys
 2 >>>
 3 >>> a = \'hello\'
 4 >>> sys.getrefcount(\'hello\')
 5 3
 6 >>> sys.getrefcount(a)
 7 2
 8 >>>
 9 >>> b = \'hello\'
10 >>> sys.getrefcount(\'hello\')
11 4
12 >>> sys.getrefcount(a)
13 3
14 >>>
15 >>> c = b
16 >>> sys.getrefcount(\'hello\')
17 5
18 >>> sys.getrefcount(a)
19 4
20 >>>

上例中第3-6行中。‘hello’的引用计数为3,首先\'hello\'被创建时引用计数为1,之后将引用赋值给了a,引用计数加1,变为2,之后通过getrefcount()函数查看引用计数时,引用计数再加1,变为3;

上句是错的,因为:

n.
>>> import sys
>>> sys.getrefcount(\'winter\')
3
>>>

不清楚为什么开始\'winter\'的引用计数就是3,日后要搞明白,可以参考

Fun with Python\'s sys.getrefcount()

同理,a在赋值语句之后引用计数为1,之后通过getrefcount()函数查看引用计数时,引用计数加1,变为2;

那么第9-13行和第15-19行都是说明了什么情况会造成引用计数的增加

2)引用计数增加的情况

  • 对象被创建:x = 3.14
  • 另外的别名被创建:y = x
  • 被作为参数传递给函数(新的本地引用):foobar(x)
  • 成为容器对象的一个元素:myList = [123, x, \'xyz\']

3)引用计数减少的情况:

  • 一个本地引用离开了其作用范围。如foobar()函数结束时
  • 对象的别名被显式销毁:del y
  • 对象的一个别名被赋值给其他对象:x = 123
  • 对象被从一个窗口对象中移除:myList.remove(x)
  • 窗口对象本身被销毁:del myList

其中del xxx会有两个结果,举例说明:

>>> import sys
>>>
>>> a = \'hello\'
>>> b = a
>>>
>>> sys.getrefcount(\'hello\')
4
>>> sys.getrefcount(a)
3
>>> sys.getrefcount(b)
3
>>>
>>> del a
>>>
>>> sys.getrefcount(\'hello\')
3
>>> sys.getrefcount(b)
2
>>> sys.getrefcount(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name \'a\' is not defined
>>>

结果是:首先从目前的命名空间删除了a,同时,b和\'hello\'的引用计数减1

2. 垃圾回收机制

吃太多,总会变胖,Python也是这样。当Python中的对象越来越多,它们将占据越来越大的内存。不过你不用太担心Python的体形,它会乖巧的在适当的时候“减肥”,启动垃圾回收(garbage collection),将没用的对象清除。在许多语言中都有垃圾回收机制,比如Java和Ruby。尽管最终目的都是塑造苗条的提醒,但不同语言的减肥方案有很大的差异 ,关于java,可参看Java内存管理与垃圾回收

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

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

1)引用计数优缺点

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

优点:

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

缺点:

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

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

 2)循环引用

包含其他对象引用的容器对象(比如:list,set,dict,class,instance)都可能产生循环引用。

首先,先看一个小例子:

 1 >>>
 2 >>> import sys
 3 >>>
 4 >>> a = []
 5 >>> b = []
 6 >>> a.append(b)
 7 >>>
 8 >>> id(b)
 9 41589704
10 >>>
11 >>> sys.getrefcount(a)
12 2
13 >>> sys.getrefcount(b)
14 3
15 >>> del b
16 >>>
17 >>>
18 >>> sys.getrefcount(a)
19 2
20 >>> id(a[0])
21 41589704
22 >>>

上例中我们看到,在a.append(b)以后,b的引用计数变为2(第14行,显示为3,实际上是2,因为getrefcount增加了引用计数);在我们del b以后,b的引用计数-1,变为1(实际应该是b引用的[]的引用计数);为了确定我们的理论正确,我们通过比较id值来说明实际上b引用的[]还在占据占据内存空间(第8行和第21行);这是该对象是可达的,因为a中还在引用他;如果最后del a的话,b引用的[]的引用计数就会再减1,变为0,就会被回收;

那么,再看一个引用计数的小例子:

 1 >>> import sys
 2 >>>
 3 >>> a = []
 4 >>> b = []
 5 >>> a.append(b)
 6 >>> b.append(a)
 7 >>>
 8 >>> sys.getrefcount(a)
 9 3
10 >>> sys.getrefcount(b)
11 3
12 >>>
13 >>> del a
14 >>> del b
15 >>>

上面的例子中,在第6行以后,可以看到a和b的引用计数都是2(忽略getrefcount增加的引用计数),那么在del a和del b以后,a和b的引用计数为1,非0;所以引用计数机制就无法回收,造成了内存泄露;

 通过“标记-清除”的方法来解决循环引用问题:

3)标记-清除

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

4)分代回收

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

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

举例说明:

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

在Python中,采用分代收集的方法。把对象分为三代,一开始,对象在创建的时候,放在一代中,如果在一次一代的垃圾检查中,改对象存活下来,就会被放到二代中,同理在一次二代的垃圾检查中,该对象存活下来,就会被放到三代中。

gc模块里面会有一个长度为3的列表的计数器,可以通过gc.get_count()获取。
例如(488,3,0),其中488是指距离上一次一代垃圾检查,Python分配内存的数目减去释放内存的数目,注意是内存分配,而不是引用计数的增加。例如:

3是指距离上一次二代垃圾检查,一代垃圾检查的次数,同理,0是指距离上一次三代垃圾检查,二代垃圾检查的次数。

gc模快有一个自动垃圾回收的阀值,即通过gc.get_threshold函数获取到的长度为3的元组,例如(700,10,10)
每一次计数器的增加,gc模块就会检查增加后的计数是否达到阀值的数目,如果是,就会执行对应的代数的垃圾检查,然后重置计数器
例如,假设阀值是(700,10,10)

    • 当计数器从(699,3,0)增加到(700,3,0),gc模块就会执行gc.collect(0),即检查一代对象的垃圾,并重置计数器为(0,4,0)
    • 当计数器从(699,9,0)增加到(700,9,0),gc模块就会执行gc.collect(1),即检查一、二代对象的垃圾,并重置计数器为(0,0,1)
    • 当计数器从(699,9,9)增加到(700,9,9),gc模块就会执行gc.collect(2),即检查一、二、三代对象的垃圾,并重置计数器为(0,0,0)

 

未完待续。。。扩展gc模块

 

以上是关于Python内存管理机制的主要内容,如果未能解决你的问题,请参考以下文章

Python如何进行内存管理

Python中的内存管理机制

Python内存管理机制

6-3 操作系统内存管理机制与Python垃圾回收面试题

python的内存管理机制

python内存管理机制