python内存管理

python中的内存管理包括对象对内存的使用,python基于C实现的底层也是使用malloc和free来进行内存管理,除了一些常规的存储以外,还有一些特殊的存储方式用于优化内存的申请释放,其中有小整数对象池、大整数对象池、字符串intern机制等。除了存储,内存释放是内存管理中十分重要的部分。python的垃圾回收主要是通过引用计数,另外通过标记清除和分代回收机制来实现解决引用计数不能处理的循环引用问题。

小整数对象池

小整数在程序中使用很频繁,python为了优化速度,提前建立好整数对象,避免了频繁申请和释放空间。建立这些整数的空间就是小整数对象池。其定义的范围为[-5,256],只要是在这个范围内的整数都是使用同一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> id(1)
39107688
>>> id(2)
39107664
>>> a=1
>>> id(a)
39107688
>>> b=a
>>> id(b)
39107688
>>> b=b+1
>>> id(b)
39107664

大整数对象池

在终端中内次都是执行一次,大整数都是重新创建,在整个文件运行的时候,文件中所有代码都加载到内存中,则同一代码块的大整数都通过大整数对象池管理,相同值使用相同对象。

1
2
3
a=1000
b=1000
print a is b //True

字符串intern机制

和之前的对象池机制一样,python会维护一个interned对象,在python中实际是一个PyDictObject,创建一个字符串的时候会先创建一个temp对象,然后通过PyDict_GetItem去查找有没有相同的字符串,有就替换,然后销毁temp,没有就通过PyDict_SetItem设置到interned对象中。

1
2
3
4
>>> a1="helloworld"
>>> a2="helloworld"
>>> a1 is a2
True

另外对于字符串有长度限制,默认是20,如果超过20会认为不常用。另外也是仅包含下划线、数字、字母的字符串,对于空格的就不支持,但也有解释器没有该限制。
字符串拼接的情况不会作为一个字符串。

1
2
3
4
5
6
7
8
9
>>> a1="helloworld"
>>> a1+="test"
>>> a2="helloworldtest"
>>> a1 is a2
False
>>> id(a1)
140455102787912
>>> id(a2)
140455102788472

垃圾回收

计数引用

对于python中的每一个对象,都会提供一个变量用于存储其引用的数量。该变量用于判断是否可以释放改对象内存空间。下面是python对象的定义

1
2
3
4
 typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;

其中ob_refcnt就是表示引用计数的变量,当一个对象的引用计数为0的时候,该对象在缓冲区则会继续等待被引用,在非缓冲区就会被释放。

1
2
3
4
5
6
7
8
9
10
>>> a=[]
>>> sys.getrefcount(a)
2
>>> b=a
>>> sys.getrefcount(a)
3
>>> del b
>>> sys.getrefcount(a)
2
>>>del

这里可以看到给a一个空的list,然后查询引用次数,可以看到为2,因为getrefcount会生成一个临时引用。然后给b赋值后会有3个引用。如果b指向其它对象或者通过del操作,则引用就会减1。(这里用一个list的原因是如果用普通的数字可能会出现很多引用的情况,这是python运行时的引用)

循环引用问题

引用计数可以消除大多数情况下不再使用的内存空间,但是不能解决循环引用。循环引用导致的原因是对象之间的相互引用,有点类似于C++头文件或者类相互包含出现初始化的问题。下面是一个循环引用的例子:

1
2
3
4
5
6
7
8
>>> l=[]
>>> sys.getrefcount(l)
2
>>> l.append(l)
>>> sys.getrefcount(l)
3
>>> l
[[...]]

可以看到在l中又引用了自己,从而造成print的死循环。这种情况下面引用计数永远都是为2,所以无法被释放。

标记清除

基于追踪回收(tracing GC)计数实现。其主要有几个步骤:
1、找到root object集合
2、从root object出发所有通过引用可达的对象标记为reachable
3、清除非reachable对象
其中寻找引用可达的过程如下:

图中黑色节点为root object,从root出发,以引用作为边,可以找到3个黄色的reachable节点,而其中灰色的节点为unreachable节点。
在python中通过gc.collect回收。

1
2
3
4
5
>>> l=[]
>>> l.append(l)
>>> del l
>>> gc.collect()
1

另外可以通过gc调试输出不同设置下的信息。下面看看通过gc回收失败的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import gc

gc.set_debug(gc.DEBUG_STATS|gc.DEBUG_COLLECTABLE|gc.DEBUG_UNCOLLECTABLE|gc.DEBUG_OBJECTS)

class A:
pass

class B:
pass

if __name__ == '__main__':
a = A()
b = B()
a.b = b
b.a = a
del a
del b
print gc.collect()
print gc.garbage

输出:

1
2
3
4
5
6
7
8
9
10
11
12
gc: collecting generation 2...
gc: objects in each generation: 601 3153 0
gc: collectable <instance 0x7f9f06dbf638>
gc: collectable <instance 0x7f9f06dbf680>
gc: collectable <dict 0x16dddc0>
gc: collectable <dict 0x16de000>
gc: done, 4 unreachable, 0 uncollectable, 0.0018s elapsed.
4
[]
gc: collecting generation 2...
gc: objects in each generation: 2 0 3419
gc: done, 0.0014s elapsed.

可以看到每个对象都包含两个unreachable,一个是instance一个是dict
如果在类里面实现del,即

1
2
3
4
5
6
7
8
class A:
def __del__(self):
pass


class B:
def __del__(self):
pass

输出:

1
2
3
4
5
6
7
8
9
10
11
12
gc: collecting generation 2...
gc: objects in each generation: 611 3153 0
gc: uncollectable <instance 0x7fa1f4709758>
gc: uncollectable <instance 0x7fa1f47097a0>
gc: uncollectable <dict 0x26adc60>
gc: uncollectable <dict 0x26b5fb0>
gc: done, 4 unreachable, 4 uncollectable, 0.0018s elapsed.
4
[<__main__.A instance at 0x7fa1f4709758>, <__main__.B instance at 0x7fa1f47097a0>]
gc: collecting generation 2...
gc: objects in each generation: 2 0 3427
gc: done, 0.0009s elapsed.

可以发现并没有被释放掉。所以在编写代码时尽可能不要自定义del
帮助文档:https://docs.python.org/2/library/gc.html
标记清除缺点:
1、需要扫描整个堆内存。

分代回收

对象引用数为0的时候不是马上进行回收,而是需要达到一定数量时才进行回收。gc采用分代回收法,将对象根据存活时间分为3代,新建对象是0代,0代经过一次自动回收后没有被释放的对象进入1代,然后同样1代没有回收的就进入2代。其回收规则如下:
1、0代对象引用数为0的对象到700时,启动一次0代对象垃圾回收;
2、10次0代回收,启动一次0和1代垃圾回收;
3、10次1代回收,启动一次0、1、2代垃圾回收。
这种设计的原因是越是前期不会被删除的对象,其生命周期也会越长。