Python的垃圾回收机制
浅谈Python的垃圾回收机制
逻辑线
- 如何访问对象?Python中万物皆是对象,利用变量访问对象,变量就是对象的一个指针(地址),指向该对象,构成一次引用。
- 什么样的对象属于需要回收的垃圾对象? 引用次数为0的对象,没有人需要它了,就需要被回收。
- 怎样获得引用次数?
sys.getrefcount(a)
方法可以获得变量a
的引用次数,基于此,我们也可以手动管理那些引用次数为0的变量。
- 仅仅依靠检测引用次数就可以实现垃圾回收了吗?不是的,由于存在相互引用、引用环等情况,引用次数不为0,也要回收资源,如果只靠检测引用次数是无法回收这类对象的。
- 如何回收相互引用和引用环这种资源呢?,实际上通过标记-清除和分代收集两种辅助机制,可以解决这些资源的回收问题。
什么是垃圾回收?
垃圾回收 = 垃圾检测 + 垃圾清除
把内存中不需要的对象删除掉,防止内存溢出,就是在进行垃圾回收,整个过程包含两个部分,先进行垃圾检测,确定哪些对象是不需要的,然后再删除掉不需要的对象。难点在第一步,如何高效的确认哪些对象是不需要的?简单来说,python采用的是引用计数机制为主,标记-清除和分代收集两种机制为辅的策略。
python中的对象
Python 中的万物皆对象,对象占用一定的内存,我们通过变量来访问一个对象,变量的本质,就是对象的一个指针(地址)。
Python中的自动内存管理
代码演示
1 |
|
运行结果如下
1 |
|
可以发现调用前后内存被自动回收了,程序员啥都不用做。
利用 sys.getrefcont()
函数进行垃圾扫描
什么是垃圾对象?引用次数为0的对象,就是不需要的对象,就可以清除掉了。
怎么确定引用次数?sys.getrefcount()
这个函数,可以查看一个变量的引用次数,因此,我们可以基于 sys.getrefcont()
进行垃圾扫描。
在函数调用一个对象时,对这个对象会产生额外的两次引用,一次来自函数栈,另一个是函数参数。
- 传参调用
1 |
|
输出
1 |
|
- 不传参调用
1 |
|
输出
1 |
|
- 删除参数之后的变化
1 |
|
输出
1 |
|
- 赋值对引用的影响
1 |
|
看到这段代码,需要你稍微注意一下,a、b、c、d、e、f、g 这些变量全部指代的是同一个对象,而 sys.getrefcount()
函数并不是统计一个指针,而是要统计一个对象被引用的次数,所以最后一共会有八次引用。
手动进行内存回收
虽然 Python 可以自动回收内存,但是我们也可以通过两行代码进行手动的内存回收。假如有一个变量 a,后面不想再用它了,那么执行两条代码搞定:
1 |
|
引用计数为 0 是否是垃圾回收的充要条件?
如果此时有面试官问:引用次数为 0 是垃圾回收启动的充要条件吗?还有没有其他可能性呢?引用计数为0是其中最简单的一种垃圾对象情况,所以引用计数为 0 是否是垃圾回收的充分非必要条件。
如果有两个对象,它们互相引用,并且不再被别的对象所引用,那么它们应该被垃圾回收,但是此时,他们的引用次数不为0。所以循环引用需要通过不可达判定,来确定是否可以回收,不能单单依靠引用计数为 0 这一个条件。
1 |
|
输出
1 |
|
可以发现,由于 a
和 b
存在相互调用,所以即使结束了函数 fuc2
的调用,内存仍然没有被释放。
试想一下,如果这段代码出现在生产环境中,哪怕 a
和 b
一开始占用的空间不是很大,但经过长时间运行后,Python 所占用的内存一定会变得越来越大,最终撑爆服务器,后果不堪设想。
当然,有人可能会说,互相引用还是很容易被发现的呀,问题不大。可是,更隐蔽的情况是出现一个引用环,在工程代码比较复杂的情况下,引用环还真不一定能被轻易发现。
如果真的怕有引用环的出现而没有检查出来的话,可以调用 gc.collect()
回收垃圾,在上述代码 func2
调用结束的位置调用 gc.collect()
后。
1 |
|
输出
1 |
|
Python的内存回收机制
上述是我们手工回收的演示,事实上 Python 可以自动处理,Python 使用标记清除(mark-sweep)算法和分代收集(generational),来启用针对循环引用的自动垃圾回收。
标记清除(mark-sweep)
标记清除(mark-sweep)是第一种垃圾回收机制,最早使用在List这种语言上,垃圾自动回收机制的出现使编程更加的简单,使得我们不需要再去考虑内存分配和释放的问题,而是更加的专注在我们产品功能的实现上。
朴素的mark-sweep思想:mark-sweep 算法是 J. McCarthy 等人在 1960 年提出并成功地应用于 Lisp 语言的标记-清除算法,JVM中也使用了该思想。朴素的mark-sweep基于先标记后清除的思想,仍以餐巾纸为例,标记-清除算法的执行过程是这样的:
午餐过程中,餐厅里的所有人都根据自己的需要取用餐巾纸。当垃圾收集机器人想收集废旧餐巾纸的时候,它会让所有用餐的人先停下来,然后,依次询问餐厅里的每一个人:“你正在用餐巾纸吗?你用的是哪一张餐巾纸?”机器人根据每个人的回答将人们正在使用的餐巾纸画上记号。询问过程结束后,机器人在餐厅里寻找所有散落在餐桌上且没有记号的餐巾纸(这些显然都是用过的废旧餐巾纸),把它们统统扔到垃圾箱里。
正如其名称所暗示的那样,标记-清除算法的执行过程分为“标记”和“清除”两大阶段。
mark-sweep 实现:是基于有向图的,对于一个有向图,如果从一个节点出发进行遍历,并标记其经过的所有节点;那么,在遍历结束后,所有没有被标记的节点,我们就称之为不可达节点。显而易见,这些节点的存在是没有任何意义的,自然的,我们就需要对它们进行垃圾回收。
Python中的 mark-sweep:每次都遍历全图,对于 Python 而言是一种巨大的性能浪费。所以,在 Python 的垃圾回收实现中,mark-sweep 使用双向链表维护了一个数据,并且只考虑容器类的对象(只有容器类对象才有可能产生循环引用)。
分代收集(generational)
原理
Python 将所有对象分为三类(代)。
刚刚创立就被回收的对象是第 0 类(代)(夭折对象);
创建之后多次GC(大于阈值m)都没有被回收的对象是第 1 类(代)(坚强对象)。
经过多次GC,那些比坚强对象活的还久的对象是第 2类(代)(不灭对象)。
从 夭折对象(临时变量等) 到 坚强对象(缓存对象、数据库连接对象等) 再到 不灭对象(加载过的核心类等),越重要的对象,活得越久(不会被清除),等级也越高。
也就是说,python根据一个对象存活的时间长短,把他划分为三个类别。可以设置不同代对象保存的阈值 gc.set_threshold(threshold0[, threshold1[, threshold2])
当某一代保存的对象超过该阈值之后就进行GC,经过这次垃圾回收后,依然存在的对象,便会依次从上一代挪到下一代。依次类推,Java里面一段固定的时间内,会对所有对象清零,Python不知道有没有类似操作。保存三个类别对象的数据结构是对应的是3个链表,它们的垃圾收集频率随着对象的存活时间的增大而减小。gc模块里面会有一个长度为3的列表的计数器,可以通过 gc.get_count()
获取。记录的就是三类对象的个数。
gc模块中与分类收集有关的函数
gc.collect([generation])
显式进行垃圾回收,可以输入参数,0代表只检查第一代的对象,1代表检查一,二代的对象,2代表检查一,二,三代的对象,如果不传参数,执行一个full collection,也就是等于传2。返回不可达(unreachable objects)对象的数目。
gc.set_threshold(threshold0[, threshold1[, threshold2])
设置自动执行垃圾回收的频率。
gc.set_debug(flags)
设置gc的debug日志,一般设置为gc.DEBUG_LEAK
gc.get_count()
获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表。
分代收集基于的思想是,新生的对象更有可能被垃圾回收,而存活更久的对象也有更高的概率继续存活。因此,通过这种做法,可以节约不少计算量,从而提高 Python 的性能。
常见的手动垃圾回收测试工具
objgraph
虽然有了自动回收机制,还是会出现内存泄露的情况。
可以通过 objgraph(一个可视化引用关系的包)。在这个包中,主要关注两个函数,第一个是 show_refs()
, 它可以生成清晰的引用关系图。
需要手动下载安装 graphviz
,然后将其 bin 目录放入到环境变量中,才能出来图片。在 jupyter notebook 中可以直接显示图片。但是在pycharm中会显示图片地址,需要自己去手动打开。
通过下面这段代码和生成的引用调用图,你能非常直观的发现,有两个list互相引用,说明这里极有可能引起内存泄漏。这样一来,再去代码层排查就容易多了。
1 |
|
总结
- 垃圾回收是 python 自带的机制,用于自动释放不会再用到的内存空间。
- python采用的是引用计数机制为主,标记-清除和分代收集两种机制为辅的策略。
- 引用计数是其中最简单的实现,这只是充分非必要条件,因为循环引用需要通过不可达判定,来确定是否可以回收。
- Python 的自动回收算法包括 标记清除 和 分代收集,主要针对的是循环引用的垃圾收集。
- 调试内存泄漏的工具:objgraph。
这些都只是皮毛
参考连接
python内存管理--垃圾回收
学习一下Python的垃圾回收
记一次面试问题——Python 垃圾回收机制
Python 源码阅读 - 垃圾回收机制
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!