>>> from env_helper import info; info()
页面更新时间: 2023-12-27 10:46:17
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-16-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

6.15. 对象的管理与垃圾回收

通常来说Python并不需要用户自己来管理内存,它与Perl、Ruby等很多动态语言一样 具备垃圾回收功能,可以自动管理内存的分配与回收,而不需要编程人员的介人。那么这样 是不是意味着用户可以高枕无忧了呢?我们来看下面一个例子:

>>> class Leak(object):
>>>     def __init__(self):
>>>         print("object with id %d was born" %id(self))
while (True):

A= Leak() B= Leak() A.b=B B.a=A A=None B=None

运行上述程序我们会发现,Python进程的内存消耗量一直在持续增长,到最后出现内存 耗光的情况。这是什么原因造成的呢?

我们先来简单谈谈Python中内存管理的方式:Python使用引用计数器(Reference counting)的方法来管理内存中的对象,即针对每一个对象维护一个引用计数值来表示该对象 当前有多少个引用。当其他对象引用该对象时,其引用计数会增加U而删除一个对当前对 象的引用,其引用计数会减1。只有当引用计数的值为0的时候该对象才会被垃圾收集器回 收,因为它表示这个对象不再被其他对象引用,是个不可达对象。引用计数算法最明显的缺 点是无法解决循环引用的问题,即两个对象相互引用。上述代码中正是由于形成了A、B对 象之间的循环引用而造成了内存泄露的情况,因为两个对象的引用计数器都不为0,该对象 并不会被垃圾收集器回收,而无限循环导致一直在申请内存而没有释放,所以最后出现了内 存耗光的情况。

循环引用常常会在列表、元组、字典、实例以及函数使用时出现。对于由循环引用而导 致的内存泄露的情况,有没有办法进行控制和管理呢?实际上Python自带了一个gc模块, 它可以用来跟踪对象的“人引用(incoming reference)”和“出引用(outgoing reference)”,并 找出复杂数据结构之间的循环引用,同时回收内存垃圾。有两种方式可以触发垃圾回收:一 种是通过显式地调用gc.collectO进行垃圾回收;还有一种是在创建新的对象为邦分配内存的 时候.检査threshold阈值,当对象的数量超过threshold的时候便自动进行垃圾回收。默认 情况下阈值设为(700,10,10),并且gc的自动回收功能是开启的,这些可以通过gc.isenabled() 査看。下面是gc模块使用的简单例子:

>>> import gc
>>> print(gc.isenabled())
True
>>> gc.isenabled()
True
>>> gc.get_threshold()
(700, 10, 10)

对于本节开头的例子,我们使用gc模块来进行垃圾回收,代码如下:

>>> import pprint
>>> import sys
>>> def main():
>>>     collected = gc.collect()
>>>     print("Garbage collector before running collected %d object." %(collected))
>>>     print("Creating reference cycles...")
>>>     A=Leak()
>>>     B=Leak()
>>>     A.b=B
>>>     B.a=A
>>>     A=None
>>>     B=None
>>>     collected = gc.collect()
>>>     pprint.pprint(gc.garbage)
>>>     print("Garbage collector after running collected %d object." %(collected))
>>> if __name__=="__main__":
>>>     ret = main()
>>>     sys.exit(ret)
Garbage collector before running collected 481 object.
Creating reference cycles...
object with id 139805067116688 was born
object with id 139805061752848 was born
[]
Garbage collector after running collected 2 object.
An exception has occurred, use %tb to see the full traceback.


SystemExit
/usr/lib/python3/dist-packages/IPython/core/interactiveshell.py:3386: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.
  warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)

gc.garbage返回的是由于循环引用而产生的不可达的垃圾对象的列表,输出为空表示内 存中此时不存在垃圾对象。gc.collect()显示所有收集和销毁的对象的数目,此处为4 (2个对 象A、B,以及其实例属性dict)。

我们再来考虑一个问题:如果我们在类Leak中添加析构方法__del__(),对象的销毁形 式和内存回收的情况是否有所不同。示例代码如下:

>>> def __del__(self):
>>>     print("object with id %d was destoryed" %id(self))

当加入了析构方法__del__()在运行程序的时候会发现gc.garbage的输出不再为空,而 是对象A、B的内存地址,也就是说这两个对象在内存中仍然以”垃圾”的形式存在。 gc.garbag()输出如下:

[<__main__.Leak object at OxOOD72BF0>, <__main__.Leak object at 0xO0D72C3O>]

这是什么原因造成的呢?实际上当存在循环引用并且当这个环中存在多个析构方法时, 垃圾回收器不能确定对象析构的顺序,所以为了安全起见仍然保持这些对象不被销毁,而当 环被打破时,gc在回收对象的时候便会再次自动调用__del__()方法。读者可以自行试验。

gc模块同时支持DEBUG模式,当设置DEBUG模式之后,对于循环引用造成的内存泄 露,gc并不释放内存,而是输出更为详细的诊断信息为发现内存泄露提供便利,从而方便程 序员进行修复。更多gc模块的使用方法读者可以参考文桂:http://docs.python.org/2Zlibrary/gc.html