在调用数据时,经常遇到内存火箭上涨的情况,而且一些变量不使用了,但是依旧占着内存,大有在其位不谋其政的意味,因此专门学习了下,并做了些实验,记录之,若不想多看,仅仅想释放内存,直接跳转到3.2和3.3即可。

1 垃圾回收

python不像C++,Java等语言一样,他们可以不用事先声明变量类型而直接对变量进行赋值。对Python语言来讲,对象的类型和内存都是在运行时确定的。这也是为什么我们称Python语言为动态类型的原因(这里我们把动态类型可以简单的归结为对变量内存地址的分配是在运行时自动判断变量类型并对变量进行赋值)。

2 引用计数

在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)。
我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。

2.1 数据引用

image.png
我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。由于上述原因,两个getrefcount将返回2和3,而不是期望的1和2。

2.2 对象引用对象

Python的一个容器对象(container),比如表、词典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。我们也可以自定义一个对象,并引用其它对象:
image.png
可以看到,a引用了对象b。
image.png
由于对象b引用了两次a,a的引用计数增加了2,使用del删除,引用也取消了。

2.3 相同数字有相同的id(x)

Id(x)见附件一。Python采用了类似Windows内核对象一样的方式来对内存进行管理。每一个对象,都维护这一个对指向该对对象的引用的计数。如图所示(图片来自Python核心编程
image.png
我们首先创建了一个对象3.14, 然后将这个浮点数对象的引用赋值给x,因为x是第一个引用,因此,这个浮点数对象的引用计数为1. 语句y = x创建了一个指向同一个对象的引用别名y,我们发现,并没有为Y创建一个新的对象,而是将Y也指向了x指向的浮点数对象,使其引用计数为2.
我们可以很容易就证明上述的观点:
image.png
变量a 和 变量b的id一致(我们可以将id值想象为C中变量的指针).对于C语言来讲,我们创建一个变量A时就会为为该变量申请一个内存空间,并将变量值 放入该空间中,当将该变量赋给另一变量B时会为B申请一个新的内存空间,并将变量值放入到B的内存空间中,这也是为什么A和B的指针不一致的原因。
而Python的情况却不一样,实际上,Python的处理方式和Javascript有点类似,如图所示,变量更像是附在对象上的标签(和引用的定义类似)。当变量被绑定在一个对象上的时候,该变量的引用计数就是1,(还有另外一些情况也会导致变量引用计数的增加),系统会自动维护这些标签,并定时扫描,当某标签的引用计数变为0的时候,该对就会被回收。
image.png

2.4 不同的数字,即使名称相同,也会有不同的id

image.png

3 内存池机制

image.png
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)等,改变一个就会引起另一个的改变,也称之为浅拷贝

深拷贝举例:
image.png
浅拷贝举例:
image.png
另外补充一个:对于字符串长短也会考虑不同的拷贝方式:
image.png

3 内存回收

3.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()。
image.png
释放使用语句:

  1. import gc
  2. del 变量名称
  3. gc.collect()

说明:若要查询那些使用的变量,可以根据附件二来。

3.2 举例使用

  1. 初始状态:

image.png

  1. 使用以下语句及变化

image.png

  1. 使用以下语句及变化

image.png

  1. 使用以下语句及变化

image.png

  1. 使用以下语句及变化

image.png

3.3 举例分析:

在第2步和4步时候分别进行了分配和销毁,但是对内存并没有影响,我猜想是python进行了优化,虽执行了,但是并没有咱开分配。只有在第3步调用时,才会占用内存资源,这样就使得在第5步时候,能够迅速释放内存。

3.4 补充

image.png
通过实验,若是执行上图的30,内存会少许增高,很快又降下来,但若使用下图的形式,内存将会在增加一倍。
image.png

说明

Python3.8版本测试如此。其他版本没有尝试。 若是jupyter中想释放掉所有内存,可以使用:
image.png

3.5 补充实例:释放所有自定义内存

image.png
注意:最后用于回收使用的变量会依然存在,在使用中若出现问题,查询是否是定义的函数变量名被释放了:比如上面的z_names_new。代码见下:

  1. def check_global_variable() -> dict:
  2. #global_variable是一个字典,存储了当前程序所有全局变量
  3. global_variable = globals()
  4. return [
  5. key for key,value in global_variable.items()\
  6. #一般不希望查看所有全局变量,因此过滤掉用户自定义以外的部分
  7. if not (
  8. key.startswith('_') \
  9. or key in ('In','Out','get_ipython','exit','quit','check_global_variable') \
  10. or type(value).__name__ in ('module','function')
  11. )
  12. ]
  13. var = check_global_variable()
  14. z_names_new = locals()
  15. for i in var:
  16. import gc
  17. del names_new[i]
  18. gc.collect()

参考文献

Python内存管理及释放:https://blog.csdn.net/jiangjiang_jian/article/details/79140742

附件

附件一:idx(x)

1、id(object)返回的是对象的“身份证号”,唯一且不变,但在不重合的生命周期里,可能会出现相同的id值。此处所说的对象应该特指复合类型的对象(如类、list等),对于字符串、整数等类型,变量的id是随值的改变而改变的。
2、一个对象的id值在CPython解释器里就代表它在内存中的地址。 is与==的区别就是, is是内存中的比较,而==是值的比较

附件二:查看全局变量

  1. def check_global_variable() -> dict:
  2. #global_variable是一个字典,存储了当前程序所有全局变量
  3. global_variable = globals()
  4. return {
  5. key:value for key,value in global_variable.items()\
  6. #一般不希望查看所有全局变量,因此过滤掉用户自定义以外的部分
  7. if not (
  8. key.startswith('_') \
  9. or key in ('In','Out','get_ipython','exit','quit','check_global_variable') \
  10. or type(value).__name__ in ('module','function')
  11. )
  12. }
  13. #自定义两个全局变量
  14. List = [1,2,3]
  15. num = 6
  16. #查看自定义的全局变量
  17. check_global_variable()

结果:

image.png
说明:如是不需要看values,可以将第5句改成: key for key,value in global_variable.items()

附件三:

GPU显存释放:torch.cuda.empty_cache()
PyTorch使用缓存内存分配器来加速内存分配。因此,nvidia-smi所显示的值通常不会反映真实的内存使用情况。
PyTorch使用缓存内存分配器来加速内存分配。这允许在没有设备同步的情况下快速释放内存。但是,由分配器管理的未使用的内存仍将显示为在nvidia-smi中使用。您可以使用memory_allocated()和max_memory_allocated()监视张量占用的内存,并使用memory_cached()和 max_memory_cached()监视由缓存分配器管理的内存。调用empty_cache()可以从PyTorch释放所有未使用的缓存内存,以便其他GPU应用程序可以使用这些内存。但是,被张量占用的GPU内存不会被释放,因此它不能增加PyTorch可用的GPU内存量。
如果您的GPU内存在Python退出后仍未释放,那么很可能某些Python子进程仍然存在。你可以通过ps -elf | grep python找到他们 并手动kill进程。
想着不kill进程的情况下全部释放内存,这个暂时没有办法处理。后面找到办法再续。

参考

https://www.aiuai.cn/aifarm1205.html