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
1] x = [
2] y = [
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个循环引用
"x1 xx" a =
"x1 xx" b =
1 a =
2 b =
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都是一样的
777 # a和b不是一样的 a =
777 b =
id(a) # 内存地址不同
140133545530064
id(b) # 内存地址不同
140133545530384
777 a = b =
id(a)
140133545530480
id(b)
140133545530480
1 # a和b是一样的 a =
1 # python的整数对象缓冲池 b =
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用于判断两个引用所指的对象是否相同。
当触发缓存机制时,只是创造了新的引用,而不是对象本身
# 示例
"xxx" a =
"xxx" b =
id(a)
140133545760616
id(b)
140133545760616
"xxx " # 注:特殊字符不能放到缓冲区 a =
"xxx " b =
id(a)
140133545760672 # 内存地址不一样
id(b)
140133545760728
"xxx_" # 注:数字、字母、下划线的组合 放在字符串缓冲区 a =
"xxx_" b =
id(a)
140133545760616 # 内存地址一样
id(b)
140133545760616
"hello world" a =
"hello world" b =
id(a) # 内存地址不一样
140133545242928
id(b)
140133545242992
"helloworld" a =
"helloworld" b =
id(a) # 内存地址一样
140133545243120
id(b)
140133545243120
"你好" a =
"你好" b =
id(a)
140612691332856
id(b)
140612688894592
# 示例:对于乘法创建的字符 只会缓冲20个
"x"*21 a =
"x"*21 b =
id(a) # 内存地址不一样
140133545742176
id(b)
140133545742248
"x"*20 # 内存地址一样 a =
"x"*20 b =
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 # 示例
"first":[1,2,3]} a = {
# 拷贝第一层数据(地址) b = a.copy()
a
{'first': [1, 2, 3]}
b
{'first': [1, 2, 3]}
id(a) # a、b引用变了
140133410603584
id(b)
140133545741768
"second"] = "No.2" a[
a
{'first': [1, 2, 3], 'second': 'No.2'}
b
{'first': [1, 2, 3]}
"first"].append(4) # a、b里面的”first”引用 没有改变 a[
# 拷贝第一层数据(地址) 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 # 示例
"first":[1,2,3]} a = {
import copy # 导入模块
b = copy.deepcopy(a)
id(a) # 内存地址不同
140133545248160
id(b)
140133410604736
"second"] = "No.2" a[
"first"].append(4) a[
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 # 示例
1,2,3] a=[
1,2,[4,5]] # 可变数据对象,有影响 b=[
c = b[:]
c
[1, 2, [4, 5]]
2].append(7) b[
b
[1, 2, [4, 5, 7]]
c
[1, 2, [4, 5, 7]]
1,2,3] # 不可变数据对象,没有影响 b=[
c = b[:]