如何在Python中引起内存泄露
前言
嗯,怎么看怎么像标题党写的标题……其实这篇文章只是笔者对Python中引用计数、弱引用的一些记录和思考,不涉及引用循环、分代回收等概念,先打个预防针。
注:本篇文章基于CPython 3.6,可能不适用于其它CPython版本或其它类型的Python实现。
引用计数
引用计数(Reference counting)可以说是一个老生常谈的问题了,炒冷饭也没啥意思。引用一下《流畅的Python》里的描述:“每个对象都会统计有多少引用指向自己。当引用计数归零时,对象立即就被销毁”。引用计数在不存在引用循环(Reference cycle)时能够及时清理内存,这也是CPython最主要的垃圾回收算法。
1 |
|
从上面的例子可以看出,set(range(3))
这个对象总共有两个引用:s1
和s2
,只有当这两个引用都无效(del s2
、del s1
)之后,这个对象才会被销毁(或者说占用的内存空间被回收)。
需要注意的是,sys.getrefcount()
返回的值会比真实引用计数值高出1,因为参数传递会(临时)增加一个引用,详见官方文档。同时,笔者用到了weakref.finalize()
方法来确认对象是否被销毁,该方法会在第一个参数指向的对象被销毁时调用第二个参数(一个Callable
对象)。
如何引起内存泄露
前面提到,引用计数在不存在引用循环时能及时清理内存,但这并不能代表程序员就能高枕无忧了。毕竟,被遗忘的引用也是引用。考虑下面这个可以追踪当前实例的类:
1 |
|
1 |
|
可以看到,即使通过del obj1
将对象的引用删除,但由于MyOBJ
这个类的__instances
属性里始终持有obj1
对应对象的另一个引用,所以这个对象始终无法被销毁。如果不能将__instances
里的引用也删除,就会引起内存泄露。
1 |
|
弱引用(weakref)
可能从第一个例子里就有读者奇怪:weakref.finalize()
会在对象被销毁时执行动作,按理说也应该持有该对象的一个引用才对,这不就产生矛盾了吗?实际上weakref
这个模块名就给出了说明:弱引用(weakref)指不增加对象引用计数的引用。于是,weakref.finalize()
自然也就不会妨碍对象被销毁了。
同样的,上一节提到的内存泄露问题也可以通过weakref
模块提供的功能解决。只需要将MyOBJ
里__instances
的类型改为WeakSet
即可:
1 |
|
1 |
|
在引入了WeakSet
后,__instances
属性持有的引用都是弱引用,当实际的对象被垃圾回收机制销毁时,__instances
属性里的引用也会跟着被删除。类似的数据结构还有WeakKeyDictionary
、WeakValueDictionary
,它们会将对象的引用分别当成字典的键和值,更具体的说明可以参考官方文档。
延伸
实际上本文只涉及Python内存管理中最基本的一些概念。关于引用循环、分代回收等概念,推荐阅读以下内容: