【py-17】python内存使用和释放
在调用数据时,经常遇到内存火箭上涨的情况,而且一些变量不使用了,但是依旧占着内存,大有在其位不谋其政的意味,因此专门学习了下,并做了些实验,记录之,若不想多看,仅仅想释放内存,直接跳转到5.2和5.3即可。
1、垃圾回收
C++,Java等语言可以不用事先声明变量类型而直接对变量进行赋值。
对Python语言来讲,对象的类型和内存都是在运行时确定的。这也是为什么我们称Python语言为动态类型的原因(这里我们把动态类型可以简单的归结为对变量内存地址的分配是在运行时自动判断变量类型并对变量进行赋值)。
2、引用计数
在Python中,每个对象都有指向该对象的引用总数,即引用计数(reference count)。
我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。
2.1 数据引用
由于上述原因,两个getrefcount将返回2和3,而不是期望的1和2。
2.2 对象引用对象
Python的一个容器对象(container),比如表、词典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。我们也可以自定义一个对象,并引用其它对象:
可以看到,a引用了对象b。
由于对象b引用了两次a,a的引用计数增加了2,使用del删除,引用也取消了。
2.3 id(x)是否相同的那些事
变量a 和 变量b的id一致(我们可以将id值想象为C中变量的指针)。对于C语言来讲,我们创建一个变量A时就会为为该变量申请一个内存空间,并将变量值 放入该空间中,当将该变量赋给另一变量B时,会为B申请一个新的内存空间,并将变量值放入到B的内存空间中,这也是为什么A和B的指针不一致的原因。
而Python的情况却不一样,实际上,Python的处理方式和Javascript有点类似,如图所示,变量更像是附在对象上的标签(和引用的定义类似)。当变量被绑定在一个对象上的时候,该变量的引用计数就是1,(还有另外一些情况也会导致变量引用计数的增加),系统会自动维护这些标签,并定时扫描,当某标签的引用计数变为0的时候,该对就会被回收。
实际测试中有一定范围,比如数值为0-256,超过也会出现id 不同,字符串也有长度限制。
a = 257
b = 257
print(a is b, id(a), id(b)) # 256内是一致的
a = 3
b = 3
print(a is b, id(a), id(b))
a = 3.14
b = 3.14
print(a is b, id(a), id(b))
## 结果
False 2483400723184 2483400722896
True 140720398635904 140720398635904
False 2483399456520 2483399456040
a = "hello"
b = "hello"
print(a is b, id(a), id(b))
a = "hello yifan"
b = "hello yifan"
print(a is b, id(a), id(b))
## 以下是结果
True 2483401461296 2483401461296
False 2483401487088 2483401492848
2.4 不同的数字,即使名称相同,也会有不同的id
3、内存池机制
Python的内存机制以金字塔行:
- -1,-2层主要由操作系统进行操作,
- 第0层是C中的malloc,free等内存分配和释放函数进行操作
- 第1层和第2层是内存池,有Python的接口函数PyMem_Malloc函数实现,当对象小于256K时有该层直接分配内存
- 第3层是最上层,也就是我们对Python对象的直接操作
在 C 中如果频繁的调用 malloc 与 free 时,是会产生性能问题的。再加上频繁的分配与释放小块的内存会产生内存碎片. Python 在这里主要干的工作有:
- 如果请求分配的内存在1~256字节之间就使用自己的内存管理系统,否则直接使用 malloc
- 这里还是会调用 malloc 分配内存,但每次会分配一块大小为 256k 的大块内存
- 经由内存池登记的内存到最后还是会回收到内存池,并不会调用 C 的 free 释放掉,以便下次使用。对于简单的Python对象,例如数值、字符串,元组(tuple不允许被更改)采用的是复制的方式 (深拷贝) ,也就是说当将另一个变量B赋值给变量A时,虽然A和B的内存空间仍然相同,但当A的值发生变化时,会重新给A分配空间,A和B的地址变得不再相同
- 而对于像字典(dict),列表(List)等,改变一个就会引起另一个的改变,也称之为 浅拷贝
4、python的深浅拷贝
最直观的理解就是:
- 深拷贝:自己新开辟了一块内存,将被拷贝内容全部拷贝过来了;copy.copy(),
- 浅拷贝:只拷贝原数据的首地址,然后通过原数据的首地址,去获取内容。copy.deepcopy()
两者的优缺点对比:
- 深拷贝拷贝程度高,将原数据复制到新的内存空间中。改变拷贝后的内容不影响原数据内容。但是深拷贝耗时长,且占用内存空间。
- 浅拷贝拷贝程度低,只复制原数据的地址。其实是将副本的地址指向原数据地址。修改副本内容,是通过当前地址指向原数据地址,去修改。所以修改副本内容会影响到原数据内容。但是浅拷贝耗时短,占用内存空间少。
总结表格:
数据形式 | 深拷贝 | 浅拷贝 |
内层可变,外层可变 | 内外层地址均发生改变 | 外层地址可改变,内层地址不变 |
内层可变,外层不可变 | 内外层地址均发生改变 | 内外层不改变 |
内层不可变,外层可变 | 外层地址改变,内层不变 | 外层地址可改变,内层地址不变 |
内层不可变,外层不可变 | 外层地址改变,内层不变 | 内外层不改变 |
注意:可变指的是可变数据类型,比如:dic, list等;不可变指不可变数据类型,比如 数值,字符串,元组
https:// blog.csdn.net/Elon15/ar ticle/details/125256787
5、内存回收
5.1 原理解释
垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率。如果内存中的对象不多,就没有必要总启动垃圾回收。所以,Python只会在特定条件下,自动启动垃圾回收。当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。我们可以通过gc模块的get_threshold()方法,查看该阈值:
import gc
print(gc.get_threshold())
返回(700, 10, 10),后面的两个10是与分代回收相关的阈值,后面可以看到。700即是垃圾回收启动的阈值。可以通过gc中的set_threshold()方法重新设置。我们也可以手动启动垃圾回收,即使用gc.collect()。
释放使用语句:
import gc
del 变量名称
gc.collect()
说明:若要查询那些使用的变量,可以根据附件二来。
5.2 举例使用
1) 初始状态:
2) 使用以下语句及变化
3) 使用以下语句及变化
4) 使用以下语句及变化
5) 使用以下语句及变化
5.3 举例分析:
在第2步和4步时候分别进行了分配和销毁,但是对内存并没有影响,我猜想是python进行了优化,虽执行了,但是并没有咱开分配。只有在第3步调用时,才会占用内存资源,这样就使得在第5步时候,能够迅速释放内存。
5.4 补充
通过实验,若是执行上图的30,内存会少许增高,很快又降下来,但若使用下图的形式,内存将会在增加一倍。
说明
Python3.8版本测试如此。其他版本没有尝试。 若是jupyter中想释放掉所有内存,可以使用:
5.5 补充实例:释放所有自定义内存
注意:最后用于回收使用的变量会依然存在,在使用中若出现问题,查询是否是定义的函数变量名被释放了:比如上面的z_names_new。代码见下:
def check_global_variable() -> dict:
#global_variable是一个字典,存储了当前程序所有全局变量
global_variable = globals()
return [
key for key,value in global_variable.items()\
#一般不希望查看所有全局变量,因此过滤掉用户自定义以外的部分
if not (
key.startswith('_') \
or key in ('In','Out','get_ipython','exit','quit','check_global_variable') \
or type(value).__name__ in ('module','function')
var = check_global_variable()
z_names_new = locals()
for i in var:
import gc
del names_new[i]
gc.collect()
附件
附件一:idx(x)
1、id(object)返回的是对象的“身份证号”,唯一且不变,但在不重合的生命周期里,可能会出现相同的id值。此处所说的对象应该特指复合类型的对象(如类、list等),对于字符串、整数等类型,变量的id是随值的改变而改变的。
2、一个对象的id值在CPython解释器里就代表它在内存中的地址。 is与==的区别就是, is是内存中的比较,而==是值的比较
附件二:查看全局变量
def check_global_variable() -> dict:
#global_variable是一个字典,存储了当前程序所有全局变量
global_variable = globals()
return {
key:value for key,value in global_variable.items()\
#一般不希望查看所有全局变量,因此过滤掉用户自定义以外的部分
if not (
key.startswith('_') \
or key in ('In','Out','get_ipython','exit','quit','check_global_variable') \
or type(value).__name__ in ('module','function')