如何在Python中引起内存泄露

前言

嗯,怎么看怎么像标题党写的标题……其实这篇文章只是笔者对Python中引用计数、弱引用的一些记录和思考,不涉及引用循环、分代回收等概念,先打个预防针。

注:本篇文章基于CPython 3.6,可能不适用于其它CPython版本或其它类型的Python实现。

引用计数

引用计数(Reference counting)可以说是一个老生常谈的问题了,炒冷饭也没啥意思。引用一下《流畅的Python》里的描述:“每个对象都会统计有多少引用指向自己。当引用计数归零时,对象立即就被销毁”。引用计数在不存在引用循环(Reference cycle)时能够及时清理内存,这也是CPython最主要的垃圾回收算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> import sys
>>> import weakref
>>> s1 = set(range(3))
>>> sys.getrefcount(s1)
2
>>> s2 = s1
>>> s2 is s1
True
>>> sys.getrefcount(s1)
3
>>> weakref.finalize(s1, lambda: print("obj has gone"))
<finalize object at ...; for 'set' at ...>
>>> del s2
>>> sys.getrefcount(s1)
2
>>> del s1
obj has gone

从上面的例子可以看出,set(range(3))这个对象总共有两个引用:s1s2,只有当这两个引用都无效(del s2del s1)之后,这个对象才会被销毁(或者说占用的内存空间被回收)。

需要注意的是,sys.getrefcount()返回的值会比真实引用计数值高出1,因为参数传递会(临时)增加一个引用,详见官方文档。同时,笔者用到了weakref.finalize()方法来确认对象是否被销毁,该方法会在第一个参数指向的对象被销毁时调用第二个参数(一个Callable对象)。

如何引起内存泄露

前面提到,引用计数在不存在引用循环时能及时清理内存,但这并不能代表程序员就能高枕无忧了。毕竟,被遗忘的引用也是引用。考虑下面这个可以追踪当前实例的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyOBJ(object):
__instances = set()

def __init__(self, name: str):
self.name = name
self.__instances.add(self)

def __str__(self) -> str:
return f"MyOBJ('{self.name}')"

@classmethod
def instances(cls):
return cls.__instances
1
2
3
4
5
6
>>> obj1 = MyOBJ("obj1")
>>> weakref.finalize(obj1, lambda: print("obj1 has gone"))
<finalize object at ...; for 'MyOBJ' at ...>
>>> del obj1
>>> len(MyOBJ.instances())
1

可以看到,即使通过del obj1将对象的引用删除,但由于MyOBJ这个类的__instances属性里始终持有obj1对应对象的另一个引用,所以这个对象始终无法被销毁。如果不能将__instances里的引用也删除,就会引起内存泄露。

1
2
3
>>> MyOBJ.instances().pop()
MyOBJ('obj1')
obj1 has gone

弱引用(weakref)

可能从第一个例子里就有读者奇怪:weakref.finalize()会在对象被销毁时执行动作,按理说也应该持有该对象的一个引用才对,这不就产生矛盾了吗?实际上weakref这个模块名就给出了说明:弱引用(weakref)指不增加对象引用计数的引用。于是,weakref.finalize()自然也就不会妨碍对象被销毁了。

同样的,上一节提到的内存泄露问题也可以通过weakref模块提供的功能解决。只需要将MyOBJ__instances的类型改为WeakSet即可:

1
2
3
4
class MyOBJ(object):
__instances = weakref.WeakSet()

# ... 省略
1
2
3
4
5
6
7
8
9
>>> obj1 = MyOBJ("obj1")
>>> weakref.finalize(obj1, lambda: print("obj1 has gone"))
<finalize object at ...; for 'MyOBJ' at ...>
>>> len(MyOBJ.instances())
1
>>> del obj1
obj1 has gone
>>> len(MyOBJ.instances())
0

在引入了WeakSet后,__instances属性持有的引用都是弱引用,当实际的对象被垃圾回收机制销毁时,__instances属性里的引用也会跟着被删除。类似的数据结构还有WeakKeyDictionaryWeakValueDictionary,它们会将对象的引用分别当成字典的键和值,更具体的说明可以参考官方文档

延伸

实际上本文只涉及Python内存管理中最基本的一些概念。关于引用循环、分代回收等概念,推荐阅读以下内容:


如何在Python中引起内存泄露
https://www.yooo.ltd/2020/06/14/如何在Python中引起内存泄露/
作者
OrangeWolf
发布于
2020年6月14日
许可协议