15. 内存管理

15.1. 变量与对象

../_images/15_varRef.png

(图片来源:https://www.cnblogs.com/geaozhang/p/7111961.html

变量

通过变量指针引用对象,变量指针指向具体对象的内存空间,取对象的值。

对象

类型已知,每个对象都包含一个头部信息(类型标识符和引用计数器)

变量名没有类型,类型属于对象。

 1>>> a = "hello"
 2>>> b = "hello"
 3>>> a == b
 4True
 5>>> a is b
 6True
 7>>> id(a)
 8140568052594368
 9>>> id(a)
10140568052594368
11
12>>> a = "hello world"
13>>> b = "hello world"
14>>> a == b
15True
16>>> a is b
17False
18>>> id(a)
19140568052594752
20>>> id(a)
21140568052594320
22
23>>> a = [1,2,3]
24>>> b = a
25>>> a == b
26True
27>>> a is b
28True

Note

Python 缓存了整数和短字符串,因此每个对象在内存中只存有一份,赋值语句只是创造新的引用,而不是对象。

Python 没有缓存长字符串、列表及其他对象,可以有多个相同的对象,赋值语句创建出新的对象。

15.1.1. 变量的改变

不可变对象

赋值、加减乘除这些操作实际上导致变量指向的对象发生了改变(已经不是指向原来的那个对象了),并不是通过这个变量来改变它指向的对象的值。

1>>> a = 10
2>>> id(a)
321856416
4>>> a = a - 1
5>>> id(a)
621856440
7>>> a *= 2
8>>> id(a)
921856224
可变对象

对于list、dict对象,此时变量的指向没有改变。

1>>> a = []
2>>> id(a)
3140568052448936
4>>> a.append(1)
5>>> id(a)
6140568052448936

15.2. 引用计数

from sys import getrefcount

使用 sys 包中的 getrefcount() ,来查看某个对象的引用计数。 需要注意的是,当使用某个引用作为参数,传递给 getrefcount() 时,参数实际上创建了一个临时的引用。 因此, getrefcount() 所得到的结果,会比期望的多 1。

15.2.1. 普通引用

 1>>> a = [1,2,3]
 2>>> getrefcount(a)
 32
 4>>> b = a
 5>>> getrefcount(a)
 63
 7>>> getrefcount(b)
 83
 9>>> del b
10>>> getrefcount(a)
112
12
13
14>>> getrefcount(1)
152418
16>>> n = 1
17>>> getrefcount(1)
182419
19>>> m = n
20>>> getrefcount(1)
212420
22>>> del n
23>>> getrefcount(1)
242419
25>>> n = [1,2,3]
26>>> getrefcount(1)
272420
28>>> m = 2
29>>> getrefcount(1)
302419

15.2.2. 容器对象

Python的容器对象(Container),比如列表、元组、字典等,可以包含多个对象。 容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。

1>>> a = [1,2,3]
2>>> getrefcount(a)
32
4>>> b = [a, a]
5>>> getrefcount(a)
64

15.2.3. 循环引用

只有容器对象才会产生循环引用的情况,比如列表、字典、用户自定义类的对象、元组等。而像数字、字符串这类简单类型不会出现循环引用。

1>>> a = []
2>>> t = [a]
3>>> getrefcount(a)
43
5>>> a.append(t)
6>>> getrefcount(a)
79

15.3. 垃圾回收

>>> import gc
>>> print gc.get_threshold()
(700, 10, 10)
## 700 是垃圾回收启动的阈值,10 是与分代回收相关的阈值

当 Python 的某个对象的引用计数降为 0 时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。 频繁的垃圾回收(Garbage Collection),将大大降低 Python 的工作效率。 如果内存中的对象不多,就没有必要总启动垃圾回收。所以,Python 只会在特定条件下,自动启动垃圾回收。

当 Python 运行时,会记录其中分配对象(Object Allocation)和取消分配对象(Object Deallocation)的次数。 当两者的差值高于某个阈值时,垃圾回收才会启动,清除那些引用计数为 0 的对象。

15.3.1. 垃圾检查

gc.get_count() 获取一个三元组,如 (488, 3, 0)

  • 488 是指距离上一次0代垃圾检查,Python 分配内存的数目减去释放内存的数目。

  • 3 是指距离上一次1代垃圾检查,0 代垃圾检查的次数。

  • 0 是指距离上一次2代垃圾检查,1 代垃圾检查的次数。

15.3.2. 分代回收

Python 将所有的对象分为 0,1,2 三代。所有的新建对象都是 0 代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。 垃圾回收启动时,一定会扫描所有的 0 代对象。如果 0 代经过一定次数垃圾回收,那么就启动对 0 代和 1 代的扫描清理。 当 1 代也经历了一定次数的垃圾回收后,那么会启动对 0、1、2,即对所有对象进行扫描。

(700, 10, 10) 表明:每 10 次 0 代垃圾回收,会配合 1 次 1 代的垃圾回收;每 10 次 1 代的垃圾回收,才会有 1 次的 2 代垃圾回收。

15.3.3. 标记-清除

Python采用了“标记-清除”(Mark and Sweep)算法,解决容器对象可能产生的循环引用问题。

  • 标记阶段:遍历所有的对象,如果是可达的(Reachable),也就是还有对象引用它,那么就标记该对象为可达;

  • 清除阶段:再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收。

15.4. 参考资料

  1. Python内存管理机制

  1. Python的内存管理

  1. Python垃圾回收机制详解

  1. 聊聊Python内存管理

  1. [Python]内存管理