Python的内存泄漏及gc模块的使用分析


Posted in Python onJuly 16, 2014

一般来说在 Python 中,为了解决内存泄漏问题,采用了对象引用计数,并基于引用计数实现自动垃圾回收。
由于Python 有了自动垃圾回收功能,就造成了不少初学者误认为自己从此过上了好日子,不必再受内存泄漏的骚扰了。但如果仔细查看一下Python文档对 __del__() 函数的描述,就知道这种好日子里也是有阴云的。下面摘抄一点文档内容如下:

Some common situations that may prevent the reference count of an object from going to zero include: circular references between objects (e.g., a doubly-linked list or a tree data structure with parent and child pointers); a reference to the object on the stack frame of a function that caught an exception (the traceback stored in sys.exc_traceback keeps the stack frame alive); or a reference to the object on the stack frame that raised an unhandled exception in interactive mode (the traceback stored in sys.last_traceback keeps the stack frame alive).

可见,有 __del__() 函数的对象间的循环引用是导致内存泄漏的主凶
另外需要说明:对没有 __del__() 函数的 Python 对象间的循环引用,是可以被自动垃圾回收掉的

如何知道一个对象是否内存泄漏了呢?

方法一、当你认为一个对象应该被销毁时(即引用计数为 0),可以通过 sys.getrefcount(obj) 来获取对象的引用计数,并根据返回值是否为 0 来判断是否内存泄漏。如果返回的引用计数不为 0,说明在此刻对象 obj 是不能被垃圾回收器回收掉的。

方法二、也可以通过 Python 扩展模块 gc 来查看不能回收的对象的详细信息。

首先,来看一段正常的测试代码:

#--------------- code begin --------------
# -*- coding: utf-8 -*-
import gc
import sys

class CGcLeak(object):
  def __init__(self):
    self._text = '#'*10

  def __del__(self):
    pass

def make_circle_ref():
  _gcleak = CGcLeak()
#  _gcleak._self = _gcleak # test_code_1
  print '_gcleak ref count0:%d' % sys.getrefcount(_gcleak)
  del _gcleak
  try:
    print '_gcleak ref count1:%d' % sys.getrefcount(_gcleak)
  except UnboundLocalError:
    print '_gcleak is invalid!'

def test_gcleak():
  # Enable automatic garbage collection.
  gc.enable()
  # Set the garbage collection debugging flags.
  gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_UNCOLLECTABLE | /
    gc.DEBUG_INSTANCES | gc.DEBUG_OBJECTS)

  print 'begin leak test...'
  make_circle_ref()

  print 'begin collect...'
  _unreachable = gc.collect()
  print 'unreachable object num:%d' % _unreachable
  print 'garbage object num:%d' % len(gc.garbage)

if __name__ == '__main__':
  test_gcleak()

在 test_gcleak() 中,设置垃圾回收器调试标志后,再用 collect() 进行垃圾回收,最后打印出该次垃圾回收发现的不可达的垃圾对象数和整个解释器中的垃圾对象数。

gc.garbage 是一个 list 对象,列表项是垃圾收集器发现的不可达(即是垃圾对象)、但又不能释放(即不能回收)的对象。文档描述为:A list of objects which the collector found to be unreachable but could not be freed (uncollectable objects).
通常,gc.garbage 中的对象是引用环中的对象。因为 Python 不知道按照什么样的安全次序来调用环中对象的 __del__() 函数,导致对象始终存活在 gc.garbage 中,造成内存泄漏。如果知道一个安全的次序,那么就打破引用环,再执行 del gc.garbage[:] ,以清空垃圾对象列表。

上段代码输出为(#后字符串为笔者所加注释):

#-----------------------------------------
begin leak test...
# 变量 _gcleak 的引用计数为 2.
_gcleak ref count0:2
# _gcleak 变为不可达(unreachable)的非法变量.
_gcleak is invalid!
# 开始垃圾回收
begin collect...
# 本次垃圾回收发现的不可达的垃圾对象数为 0.
unreachable object num:0
# 整个解释器中的垃圾对象数为 0.
garbage object num:0
#-----------------------------------------

由此可见 _gcleak 对象的引用计数是正确的,也没有任何对象发生内存泄漏。

如果不注释掉 make_circle_ref() 中的 test_code_1 语句:

_gcleak._self = _gcleak

也就是让 _gcleak 形成一个自己对自己的循环引用。再运行上述代码,输出结果就变成:

#-----------------------------------------
begin leak test...
_gcleak ref count0:3
_gcleak is invalid!
begin collect...
# 发现可以回收的垃圾对象: 地址为 012AA090,类型为 CGcLeak.
gc: uncollectable <CGcLeak 012AA090>
gc: uncollectable <dict 012AC1E0>
unreachable object num:2
#!! 不能回收的垃圾对象数为 1,导致内存泄漏!
garbage object num:1
#-----------------------------------------

可见 <CGcLeak 012AA090> 对象发生了内存泄漏!!而多出的 dict 垃圾就是泄漏的 _gcleak 对象的字典,打印出字典信息为:

{'_self': <__main__.CGcLeak object at 0x012AA090>, '_text': '##########'}

除了对自己的循环引用,多个对象间的循环引用也会导致内存泄漏。简单举例如下:

#--------------- code begin --------------

class CGcLeakA(object):
  def __init__(self):
    self._text = '#'*10

  def __del__(self):
    pass

class CGcLeakB(object):
  def __init__(self):
    self._text = '*'*10

  def __del__(self):
    pass

def make_circle_ref():
  _a = CGcLeakA()
  _b = CGcLeakB()
  _a._b = _b # test_code_2
  _b._a = _a # test_code_3
  print 'ref count0:a=%d b=%d' % /
    (sys.getrefcount(_a), sys.getrefcount(_b))
#  _b._a = None  # test_code_4
  del _a
  del _b
  try:
    print 'ref count1:a=%d' % sys.getrefcount(_a)
  except UnboundLocalError:
    print '_a is invalid!'
  try:
    print 'ref count2:b=%d' % sys.getrefcount(_b)
  except UnboundLocalError:
    print '_b is invalid!'

#--------------- code end ----------------

这次测试后输出结果为:

#-----------------------------------------
begin leak test...
ref count0:a=3 b=3
_a is invalid!
_b is invalid!
begin collect...
gc: uncollectable <CGcLeakA 012AA110>
gc: uncollectable <CGcLeakB 012AA0B0>
gc: uncollectable <dict 012AC1E0>
gc: uncollectable <dict 012AC0C0>
unreachable object num:4
garbage object num:2
#-----------------------------------------

可见 _a,_b 对象都发生了内存泄漏。因为二者是循环引用,垃圾回收器不知道该如何回收,也就是不知道该首先调用那个对象的 __del__() 函数。

采用以下任一方法,打破环状引用,就可以避免内存泄漏:

1.注释掉 make_circle_ref() 中的 test_code_2 语句;
2.注释掉 make_circle_ref() 中的 test_code_3 语句;
3.取消对 make_circle_ref() 中的 test_code_4 语句的注释。

相应输出结果变为:

#-----------------------------------------
begin leak test...
ref count0:a=2 b=3 # 注:此处输出结果视情况变化.
_a is invalid!
_b is invalid!
begin collect...
unreachable object num:0
garbage object num:0
#-----------------------------------------

结论:Python 的 gc 有比较强的功能,比如设置 gc.set_debug(gc.DEBUG_LEAK) 就可以进行循环引用导致的内存泄露的检查。如果在开发时进行内存泄露检查;在发布时能够确保不会内存泄露,那么就可以延长 Python 的垃圾回收时间间隔、甚至主动关闭垃圾回收机制,从而提高运行效率。

Python 相关文章推荐
python删除特定文件的方法
Jul 30 Python
Python3.X 线程中信号量的使用方法示例
Jul 24 Python
django开发教程之利用缓存文件进行页面缓存的方法
Nov 10 Python
python matplotlib中文显示参数设置解析
Dec 15 Python
python实现k-means聚类算法
Feb 23 Python
python实现数据导出到excel的示例--普通格式
May 03 Python
Python中staticmethod和classmethod的作用与区别
Oct 11 Python
简单了解Django应用app及分布式路由
Jul 24 Python
基于YUV 数据格式详解及python实现方式
Dec 09 Python
后端开发使用pycharm的技巧(推荐)
Mar 27 Python
浅谈python量化 双均线策略(金叉死叉)
Jun 03 Python
python对批量WAV音频进行等长分割的方法实现
Sep 25 Python
Python的垃圾回收机制深入分析
Jul 16 #Python
python中将字典转换成其json字符串
Jul 16 #Python
记录Django开发心得
Jul 16 #Python
Python实现动态添加类的属性或成员函数的解决方法
Jul 16 #Python
Python重新引入被覆盖的自带function
Jul 16 #Python
Python实现扫描指定目录下的子目录及文件的方法
Jul 16 #Python
python re正则表达式模块(Regular Expression)
Jul 16 #Python
You might like
星际争霸秘籍
2020/03/04 星际争霸
在“咖啡之国”感受咖啡文化
2021/03/03 咖啡文化
php数组函数序列之each() - 获取数组当前内部指针所指向元素的键名和键值,并将指针移到下一位
2011/10/31 PHP
mantis安装、配置和使用中的问题小结
2014/07/14 PHP
PHP实现递归无限级分类
2015/10/22 PHP
Laravel自定义 封装便捷返回Json数据格式的引用方法
2019/09/29 PHP
js实现获取焦点后光标在字符串后
2014/09/17 Javascript
javascript十六进制及二进制转化的方法
2015/05/06 Javascript
javascript实现unicode与ASCII相互转换的方法
2015/12/10 Javascript
JavaScript表单验证实例之验证表单项是否为空
2016/01/10 Javascript
Javascript中的几种继承方式对比分析
2016/03/22 Javascript
Struts2+jquery.form.js实现图片与文件上传的方法
2016/05/05 Javascript
node.js中http模块和url模块的简单介绍
2017/10/06 Javascript
关闭Vue计算属性自带的缓存功能方法
2018/03/02 Javascript
js实现下拉框二级联动
2018/12/04 Javascript
LayUi数据表格自定义赋值方式
2019/10/26 Javascript
Flask SQLAlchemy一对一,一对多的使用方法实践
2013/02/10 Python
Python中isnumeric()方法的使用简介
2015/05/19 Python
python列表操作之extend和append的区别实例分析
2015/07/28 Python
Python selenium如何设置等待时间
2016/09/15 Python
Python3.6中Twisted模块安装的问题与解决
2019/04/15 Python
关于Python 常用获取元素 Driver 总结
2019/11/24 Python
Python中的sys.stdout.write实现打印刷新功能
2020/02/21 Python
美国潜水装备、水肺潜水和浮潜设备商店:Leisure Pro
2018/08/08 全球购物
如果NULL和0作为空指针常数是等价的,那我到底该用哪一个
2014/09/16 面试题
师范生教师实习自我鉴定
2013/09/27 职场文书
传播学毕业生求职信
2013/10/11 职场文书
写给女朋友的道歉信
2014/01/12 职场文书
社区包粽子活动方案
2014/01/21 职场文书
火车的故事教学反思
2014/02/11 职场文书
教师开学感言
2014/02/14 职场文书
诚信承诺书范文
2014/03/27 职场文书
军人离婚协议书样本
2014/10/21 职场文书
穷人该怎么创业?谨记以下几点
2019/07/11 职场文书
tensorboard 可视化之localhost:6006不显示的解决方案
2021/05/22 Python
python引入其他文件夹下的py文件具体方法
2021/05/23 Python