热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

聊一聊Python中的“垃圾”回收

作者:heroyfwww.heroyf.club20191023python_gc前言对于python来说,一切皆为对象,所有的变量赋值都

作者:heroyf

www.heroyf.club/2019/10/23/python_gc/

前言

对于python来说,一切皆为对象,所有的变量赋值都遵循着对象引用机制。程序在运行的时候,需要在内存中开辟出一块空间,用于存放运行时产生的临时变量;计算完成后,再将结果输出到永久性存储器中。如果数据量过大,内存空间管理不善就很容易出现 OOM(out of memory),俗称爆内存,程序可能被操作系统中止。

而对于服务器,内存管理则显得更为重要,不然很容易引发内存泄漏- 这里的泄漏,并不是说你的内存出现了信息安全问题,被恶意程序利用了,而是指程序本身没有设计好,导致程序未能释放已不再使用的内存。- 内存泄漏也不是指你的内存在物理上消失了,而是意味着代码在分配了某段内存后,因为设计错误,失去了对这段内存的控制,从而造成了内存的浪费。也就是这块内存脱离了gc的控制

计数引用

因为python中一切皆为对象,你所看到的一切变量,本质上都是对象的一个指针。

当一个对象不再调用的时候,也就是当这个对象的引用计数(指针数)为 0 的时候,说明这个对象永不可达,自然它也就成为了垃圾,需要被回收。可以简单的理解为没有任何变量再指向它。

  1. import os

  2. import psutil

  3. # 显示当前 python 程序占用的内存大小

  4. def show_memory_info(hint):

  5. pid = os.getpid()

  6. p = psutil.Process(pid)

  7. info = p.memory_full_info()

  8. memory = info.uss / 1024./ 1024

  9. print( {} memory used: {} MB .format(hint, memory))

  10. def func():

  11. show_memory_info( initial )

  12. a = [i for i in range(10000000)]

  13. show_memory_info( after a created )

  14. func()

  15. show_memory_info( finished )

  16. ########## 输出 ##########

  17. initial memory used: 47.19140625 MB

  18. after a created memory used: 433.91015625 MB

  19. finished memory used: 48.109375 MB

可以看到调用函数 func(),在列表 a 被创建之后,内存占用迅速增加到了 433 MB:而在函数调用结束后,内存则返回正常。这是因为,函数内部声明的列表 a 是局部变量,在函数返回后,局部变量的引用会注销掉;此时,列表 a 所指代对象的引用数为 0,Python 便会执行垃圾回收,因此之前占用的大量内存就又回来了。

  1. def func():

  2. show_memory_info( initial )

  3. global a

  4. a = [i for i in range(10000000)]

  5. show_memory_info( after a created )

  6. func()

  7. show_memory_info( finished )

  8. ########## 输出 ##########

  9. initial memory used: 48.88671875 MB

  10. after a created memory used: 433.94921875 MB

  11. finished memory used: 433.94921875 MB

新的这段代码中,global a 表示将 a 声明为全局变量。那么,即使函数返回后,列表的引用依然存在,于是对象就不会被垃圾回收掉,依然占用大量内存。同样,如果我们把生成的列表返回,然后在主程序中接收,那么引用依然存在,垃圾回收就不会被触发,大量内存仍然被占用着:

  1. def func():

  2. show_memory_info( initial )

  3. a = [i for i in derange(10000000)]

  4. show_memory_info( after a created )

  5. return a

  6. a = func()

  7. show_memory_info( finished )

  8. ########## 输出 ##########

  9. initial memory used: 47.96484375 MB

  10. after a created memory used: 434.515625 MB

  11. finished memory used: 434.515625 MB

那怎么可以看到变量被引用了多少次呢?通过 sys.getrefcount

  1. import sys

  2. a = []

  3. # 两次引用,一次来自 a,一次来自 getrefcount

  4. print(sys.getrefcount(a))

  5. def func(a):

  6. # 四次引用,a,python 的函数调用栈,函数参数,和 getrefcount

  7. print(sys.getrefcount(a))

  8. func(a)

  9. # 两次引用,一次来自 a,一次来自 getrefcount,函数 func 调用已经不存在

  10. print(sys.getrefcount(a))

  11. ########## 输出 ##########

  12. 2

  13. 4

  14. 2

如果其中涉及函数调用,会额外增加两次1. 函数栈2. 函数调用

从这里就可以看到python不再需要像C那种的认为的释放内存,但是python同样给我们提供了手动释放内存的方法 gc.collect()

  1. import gc

  2. show_memory_info( initial )

  3. a = [i for i in range(10000000)]

  4. show_memory_info( after a created )

  5. del a

  6. gc.collect()

  7. show_memory_info( finish )

  8. print(a)

  9. ########## 输出 ##########

  10. initial memory used: 48.1015625 MB

  11. after a created memory used: 434.3828125 MB

  12. finish memory used: 48.33203125 MB

  13. ---------------------------------------------------------------------------

  14. NameErrorTraceback(most recent call last)

  15. in

  16. 11

  17. 12 show_memory_info( finish )

  18. ---> 13print(a)

  19. NameError: name a isnotdefined

截止目前,貌似python的垃圾回收机制非常的简单,只要对象引用次数为0,必定为触发gc,那么引用次数为0是否是触发gc的充要条件呢?

循环回收

如果有两个对象,它们互相引用,并且不再被别的对象所引用,那么它们应该被垃圾回收吗?

  1. def func():

  2. show_memory_info( initial )

  3. a = [i for i in range(10000000)]

  4. b = [i for i in range(10000000)]

  5. show_memory_info( after a, b created )

  6. a.append(b)

  7. b.append(a)

  8. func()

  9. show_memory_info( finished )

  10. ########## 输出 ##########

  11. initial memory used: 47.984375 MB

  12. after a, b created memory used: 822.73828125 MB

  13. finished memory used: 821.73046875 MB

从结果显而易见,它们并没有被回收,但是从程序上来看,当这个函数结束的时候,作为局部变量的a,b就已经从程序意义上不存在了。但是因为它们的互相引用,导致了它们的引用数都不为0。

这时要如何规避呢1. 从代码逻辑上进行整改,避免这种循环引用2. 通过人工回收

  1. import gc

  2. def func():

  3. show_memory_info( initial )

  4. a = [i for i in range(10000000)]

  5. b = [i for i in range(10000000)]

  6. show_memory_info( after a, b created )

  7. a.append(b)

  8. b.append(a)

  9. func()

  10. gc.collect()

  11. show_memory_info( finished )

  12. ########## 输出 ##########

  13. initial memory used: 49.51171875 MB

  14. after a, b created memory used: 824.1328125 MB

  15. finished memory used: 49.98046875 MB

python针对循环引用,有它的自动垃圾回收算法1. 标记清除(mark-sweep)算法2. 分代收集(generational)

标记清除

标记清除的步骤总结为如下步骤1. GC会把所有的『活动对象』打上标记2. 把那些没有标记的对象『非活动对象』进行回收

那么python如何判断何为非活动对象?

通过用图论来理解不可达的概念。对于一个有向图,如果从一个节点出发进行遍历,并标记其经过的所有节点;那么,在遍历结束后,所有没有被标记的节点,我们就称之为不可达节点。显而易见,这些节点的存在是没有任何意义的,自然的,我们就需要对它们进行垃圾回收。

但是每次都遍历全图,对于 Python 而言是一种巨大的性能浪费。所以,在 Python 的垃圾回收实现中,mark-sweep 使用双向链表维护了一个数据结构,并且只考虑容器类的对象(只有容器类对象,list、dict、tuple,instance,才有可能产生循环引用)。

图中把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

分代回收

分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时(当垃圾回收器中新增对象减去删除对象达到相应的阈值时),Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。

事实上,分代回收基于的思想是,新生的对象更有可能被垃圾回收,而存活更久的对象也有更高的概率继续存活。因此,通过这种做法,可以节约不少计算量,从而提高 Python 的性能。

所以对于刚刚的问题,引用计数只是触发gc的一个充分非必要条件,循环引用同样也会触发。

调试

可以使用 objgraph来调试程序,因为目前它的官方文档,还没有细读,只能把文档放在这供大家参阅啦~

其中两个函数非常有用 1. show_refs() 2. show_backrefs()

总结

  1. 垃圾回收是 Python 自带的机制,用于自动释放不会再用到的内存空间;

  2. 引用计数是其中最简单的实现,不过切记,这只是充分非必要条件,因为循环引用需要通过不可达判定,来确定是否可以回收;

  3. Python 的自动回收算法包括标记清除和分代回收,主要针对的是循环引用的垃圾收集;

  4. 调试内存泄漏方面, objgraph 是很好的可视化分析工具。

推荐阅读

“我靠这9个百度AI项目,拿到了年薪30万+的大厂offer”

从词袋到transfomer,梳理十年Kaggle竞赛,看自然语言处理的变迁史

Python 海象运算符 (:=) 的三种用法

GitHub热榜 | AI黑客教程来了,破解神经网络、攻击GPU

Pandas | 5 种技巧高效利用value-counts



推荐阅读
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • Python瓦片图下载、合并、绘图、标记的代码示例
    本文提供了Python瓦片图下载、合并、绘图、标记的代码示例,包括下载代码、多线程下载、图像处理等功能。通过参考geoserver,使用PIL、cv2、numpy、gdal、osr等库实现了瓦片图的下载、合并、绘图和标记功能。代码示例详细介绍了各个功能的实现方法,供读者参考使用。 ... [详细]
  • 本文介绍了在Python中使用FOR循环实现用户输入错误值3次后终止程序的方法。作者提到了自己对这个问题的困惑和尝试,并给出了解决方案。该方案要求代码必须包含FOR循环,但作者不确定是需要一个FOR循环还是3个FOR循环。最后,作者还给出了一些示例代码来说明如何将英里转换为公里和将英寸转换为厘米。 ... [详细]
  • 超级简单加解密工具的方案和功能
    本文介绍了一个超级简单的加解密工具的方案和功能。该工具可以读取文件头,并根据特定长度进行加密,加密后将加密部分写入源文件。同时,该工具也支持解密操作。加密和解密过程是可逆的。本文还提到了一些相关的功能和使用方法,并给出了Python代码示例。 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • 本文介绍了蓝桥训练中的闰年判断问题,并提供了使用Python代码进行判断的方法。根据给定的年份,判断是否为闰年的条件是:年份是4的倍数且不是100的倍数,或者是400的倍数。根据输入的年份,输出结果为yes或no。本文提供了相应的Python代码实现。 ... [详细]
  • 本文介绍了如何使用python从列表中删除所有的零,并将结果以列表形式输出,同时提供了示例格式。 ... [详细]
  • 单点登录原理及实现方案详解
    本文详细介绍了单点登录的原理及实现方案,其中包括共享Session的方式,以及基于Redis的Session共享方案。同时,还分享了作者在应用环境中所遇到的问题和经验,希望对读者有所帮助。 ... [详细]
  • Python SQLAlchemy库的使用方法详解
    本文详细介绍了Python中使用SQLAlchemy库的方法。首先对SQLAlchemy进行了简介,包括其定义、适用的数据库类型等。然后讨论了SQLAlchemy提供的两种主要使用模式,即SQL表达式语言和ORM。针对不同的需求,给出了选择哪种模式的建议。最后,介绍了连接数据库的方法,包括创建SQLAlchemy引擎和执行SQL语句的接口。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
author-avatar
红颜弹指老a刹那芳华_623
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有