1
2
3
4
5
Python内存管理三大块
○ 引用计数
○ 垃圾回收
○ 内存池
Python的内存管理以引用计数为主,垃圾回收为辅,还有个内存池

一. 引用机制

● 引用机制

Python动态类型

对象是储存在内存中的实体。

○ 我们在程序中写的对象名,只是指向这一对象的引用(reference)

引用和对象分离,是动态类型的核心

○ 引用可以随时指向一个新的对象(内存地址会不一样)


二. 引用计数

● 引用计数

在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)

引用计数器原理

○ 每个对象维护一个 ob_ref 字段,用来记录该对象当前被引用的次数 每当新的引用指向该对象时,它的引用计数ob_ref加1

○ 每当该对象的引用失效时计数ob_ref减1

一旦对象的引用计数为0,该对象可以被回收,对象占用的内存空间将被释放。 它的缺点是需要额外的空间维护引用计数,这个问题是其次的

○ 最主要的问题是它不能解决对象的“循环引用”

1
2
3
# 示例
# a = 1 , b = 1 ,1的引用计数为2(保存它被引用的次数)
# a = 2 , b = 3 , 1的引用计数为0(内存里面不需要它了,回收销毁,这块对象被回收了,对象占用的内存空间将被释放)
1
2
3
4
5
6
7
8
获取引用计数: getrefcount()
○ 当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,
getrefcount()所得到的结果,会比期望的多1

# 示例
from sys import getrefcount # 导入模块
a = [1,2,3]
print(getrefcount(a)) # 获取对象a的引用计数 , 结果为2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
增加引用计数
○ 当一个对象A被另一个对象B引用时,A的引用计数将增加1


减少引用计数
del删除或重新引用时,引用计数会变化(del只是删除引用)

# 示例
from sys import getrefcount
a = [1,2,3] # 真实引用计数:1
b = a # 真实引用计数:2
c = [a,a] # 真实引用计数:4
del c[0] # del删除引用 引用计数 - 1 ; 真实引用计数: 3
print(c) # c 是列表对象 输出为 [[1, 2, 3]]

print(getrefcount(a)) # 引用计数为4,真实引用计数为3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
循环引用的情况
x = []
y = []
x.append(y) y.append(x)
○ 对于上面相互引用的情况,如果不存在其他对象对他们的引用,这两个对象所占用的内存也还是无法回收,从而导致内存泄漏

# 示例1
>>> x = [1]
>>> y = [2]

>>> x.append(y)
>>> x
[1, [2]]
>>> y.append(x)
>>> y
[2, [1, [...]]] # 注:发生死循环

# 示例2
>>> from sys import getrefcount
>>> x = ["x"]
>>> y = ["y"]
>>> getrefcount(x)
2
>>> getrefcount(y)
2
>>> x.append(y)
>>> getrefcount(x)
2
>>> getrefcount(y)
3
>>> y.append(x)
>>> getrefcount(x)
3
>>> x
['x', ['y', [...]]]
>>> y
['y', ['x', [...]]]
>>> del x
>>> y
['y', ['x', [...]]]
>>> del y # del x;del y引用删除,这块内存区域获取不到了
1
2
3
引用计数机制的优点:
○ 简单
○ 实时性
1
2
3
引用计数机制的缺点:
○ 维护引用计数消耗资源
○ 循环引用时,无法回收

三. 垃圾回收

● 垃圾回收

回收原则

○ 当Python的某个对象的引用计数降为0时,可以被垃圾回收

gc机制

○ GC作为现代编程语言的自动内存管理机制,专注于两件事

○ 找到内存中无用的垃圾资源

○ 清除这些垃圾并把内存让出来给其他对象使用

GC彻底把程序员从资源管理的重担中解放出来,让他们有更多的时间放在业务逻辑上。但这并不意味着码农就可以不去了解GC,毕竟多了解GC知识还是有利于我们写出更健壮的代码

效率问题

○ 垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率

○ 当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 示例
>>> import gc
>>> print(gc.get_threshold())
(700, 10, 10) # 注:默认值

# 示例
>>> del x
>>> del y
>>> gc.collect()
2 # 删除了2个循环引用

>>> a = "x1 xx"
>>> b = "x1 xx"
>>> a = 1
>>> b = 2
>>> gc.collect()
0
1
2
3
4
三种情况触发垃圾回收
○ 调用gc.collect()
○ GC达到阀值时
○ 程序退出时
1
2
3
4
5
6
7
8
分代(generation)回收
这一策略的基本假设是:存活时间越久的对象,越不可能在后面的程序中变成垃圾
○ Python将所有的对象分为0,1,2三代
○ 所有的新建对象都是0代对象
○ 当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象
○ 垃圾回收启动时,一定会扫描所有的0代对象
○ 如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理
○ 当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描

标记清除

​ 标记-清除机制,顾名思义,首先标记对象(垃圾检测),然后清除垃圾(垃圾回收)。

​ 主要用于解决循环引用

○ 1.标记:活动(有被引用), 非活动(可被删除)

○ 2.清除:清除所有非活动的对象


四. 缓冲池

● 缓冲池

整数对象缓冲池

○ 对于==[-5,256]== 这样的小整数,系统已经初始化好,可以直接拿来用。而对于其他的大整数,系统则提前申请了一块内存空间,等需要的时候在这上面创建大整数对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 示例:小整数而言 id都是一样的
>>> a = 777 # a和b不是一样的
>>> b = 777
>>> id(a) # 内存地址不同
140133545530064
>>> id(b) # 内存地址不同
140133545530384
>>> a = b = 777
>>> id(a)
140133545530480
>>> id(b)
140133545530480
>>> a = 1 # a和b是一样的
>>> b = 1 # python的整数对象缓冲池
>>> id(a)
140133544871840 # 内存地址一样
>>> id(b)
140133544871840 # 内存地址一样
>>> from sys import getrefcount
>>> getrefcount(a)
801
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
字符串缓存
○ 为了检验两个引用指向同一个对象,我们可以用is关键字。is用于判断两个引用所指的对象是否相同。
当触发缓存机制时,只是创造了新的引用,而不是对象本身

# 示例
>>> a = "xxx"
>>> b = "xxx"
>>> id(a)
140133545760616
>>> id(b)
140133545760616
>>> a = "xxx " # 注:特殊字符不能放到缓冲区
>>> b = "xxx "
>>> id(a)
140133545760672 # 内存地址不一样
>>> id(b)
140133545760728
>>> a = "xxx_" # 注:数字、字母、下划线的组合 放在字符串缓冲区
>>> b = "xxx_"
>>> id(a)
140133545760616 # 内存地址一样
>>> id(b)
140133545760616
>>> a = "hello world"
>>> b = "hello world"
>>> id(a) # 内存地址不一样
140133545242928
>>> id(b)
140133545242992
>>> a = "helloworld"
>>> b = "helloworld"
>>> id(a) # 内存地址一样
140133545243120
>>> id(b)
140133545243120

>>> a = "你好"
>>> b = "你好"
>>> id(a)
140612691332856
>>> id(b)
140612688894592


# 示例:对于乘法创建的字符 只会缓冲20个
>>> a = "x"*21
>>> b = "x"*21
>>> id(a) # 内存地址不一样
140133545742176
>>> id(b)
140133545742248
>>> a = "x"*20 # 内存地址一样
>>> b = "x"*20
>>> id(a)
140133545246768
>>> id(b)
140133545246768

注意

○ 这对于经常使用的数字和字符串来说也是一种优化的方案

字符串的intern机制

○ python对于短小的,只含有字母数字的字符串自动触发缓存机制。其他情况不会缓存


五. 深拷贝与浅拷贝

● 深拷贝与浅拷贝

浅拷贝

○ 拷贝第一层数据(地址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 示例
>>> a = {"first":[1,2,3]}
>>> b = a.copy() # 拷贝第一层数据(地址)
>>> a
{'first': [1, 2, 3]}
>>> b
{'first': [1, 2, 3]}
>>> id(a) # a、b引用变了
140133410603584
>>> id(b)
140133545741768
>>> a["second"] = "No.2"
>>> a
{'first': [1, 2, 3], 'second': 'No.2'}
>>> b
{'first': [1, 2, 3]}
>>> a["first"].append(4) # a、b里面的”first”引用 没有改变
>>> a # 拷贝第一层数据(地址)
{'first': [1, 2, 3, 4], 'second': 'No.2'}
>>> b
{'first': [1, 2, 3, 4]}
>>> id(a["first"]) # 第一层数据(地址) 内存地址相同
140133413100296
>>> id(b["first"])
140133413100296

深拷贝

○ 递归拷贝所有层的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 示例
>>> a = {"first":[1,2,3]}
>>> import copy # 导入模块
>>> b = copy.deepcopy(a)
>>> id(a) # 内存地址不同
140133545248160
>>> id(b)
140133410604736
>>> a["second"] = "No.2"
>>> a["first"].append(4)
>>> a
{'first': [1, 2, 3, 4], 'second': 'No.2'} # 递归拷贝所有层的数据
>>> b
{'first': [1, 2, 3]} # 递归拷贝所有层的数据

小结

○ 数字和字符串、元组,不能改变对象本身,只能改变引用的指向,称为不可变数据对象(immutable object)

○ 列表、字典、集合可以通过引用其元素,改变对象自身(in-place change)。这种对象类型,称为可变数据对象(mutable object)

1
2
3
4
5
6
7
8
9
10
11
12
13
# 示例
>>> a=[1,2,3]
>>> b=[1,2,[4,5]] # 可变数据对象,有影响
>>> c = b[:]
>>> c
[1, 2, [4, 5]]
>>> b[2].append(7)
>>> b
[1, 2, [4, 5, 7]]
>>> c
[1, 2, [4, 5, 7]]
>>> b=[1,2,3] # 不可变数据对象,没有影响
>>> c = b[:]