内存管理机制
Python的内存管理内存总共分为4层(Layer0-3):
第一层Layer1的仅仅是对malloc的简单包装,raw memory,目的是为了兼容各个操作系统,因为不同的操作系统调用malloc的时候可能会有不同的行为结果;第二层Layer2是内存管理机制的核心,其中gc就是在这一层发挥至关重要的作用。第三层,是对象缓冲池,如python对一些对象的直接操作,包括int,list等。
对于可能被经常使用、而且是immutable的对象,如bool类型,元祖类型,小的整数、长度较短的字符串等,python会缓存在layer3,直接供python调用,避免频繁创建和销毁。
>>> a,b=1234567890123,1234567890123
>>> a is b
True
>>> a,b=(1,2,3,'a'),(1,2,3,'a')
>>> a is b
False
>>> a,b=('a'),('a')
>>> a is b
True
当一个对象逻辑上不被使用了,但并没有被释放,那么就存在内存泄露,很有可能会造成程序效率低下甚至崩溃;
Python分配内存的时候又分为大内存和小内存。大小以256字节为界限,对于大内存使用Malloc进行分配,而对于小内存则使用内存池进行分配。由于小内存的分配和释放是频繁的,因此内存池的使用大大提高了python的执行效率。
引用计数
在python中大多数对象的生命周期都是通过引用计数来管理的,引用计数也是一种最直观最简单的垃圾收集技术
每个python对象都有一个引用计数器,用于记录多少变量指向这个对象,可以通过sys模块的getrefcount查询获得。
>>> sys.getrefcount({'a':1})
1
>>> sys.getrefcount(1)
590
每一个对象都会维护一个引用计数器,当一个对象被引用的时候,它的计数器就+1,当一个对象的引用被销毁时,计数器-1,当这个对象的引用计数为0的时候,说明这个对象已经没有使用了,可以被释放,就会被回收,具有实时性。由于引用计数需要维护计数器等额外的操作,为了与引用计数搭配,在内存的分配和释放上获得最高的效率,python因此设计了大量的内存池机制。
下面这些情况引用计数+1:
- 对象被创建:a=4
- 引用被复制:y=x
- 被作为参数传递给函数:f(x)
- 作为容器对象的一个元素:a=[1,x]
下面这些情况引用计数-1
- 离开作用域。比如f(x)函数结束时,x指向的对象引用减1。
- 引用被显式的销毁:del x
- 对象的一个别名被赋值给其他对象:y=1
- 对象从一个容器对象中移除:l.remove(x)
- 容器对象本身被销毁:del l。
python 的内存管理主要以引用计数为主,引用计数机制能释放大部分无用对象,除了一种情况,循环引用,因为循环引用的对象引用计数器永不为0.
循环引用,就是一个对象直接或者间接引用自己本身,导致计数器不为0:
class Test(object):
pass
t1 = Test()
t1.a = t1
a, b = Test(), Test()
a.attr_b = b
b.attr_a = a
l1=[]
l2=[]
l1.append(l2)
l2.append(l1)
标记清除
标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。标记清除和分代回收就是为了解决循环引用而生的。
标记清除会使用垃圾收集监控对象,讲对象放到链表上,被垃圾收集监控的对象并非只有垃圾收集机制才能回收,正常的引用计数就能销毁一个被纳入垃圾收集机制监控的对象。虽然有很多对象挂在垃圾收集机制监控的链表上,但实际更多时候,是引用计数机制在维护这些对象,只有对引用计数无能为力的循环引用,垃圾收集机制才起作用,事实上,除循环引用外的对象,垃圾收集机制是无能为力的,因为挂在垃圾收集机制上的对象都是引用计数不为0的,如果是0,早就被引用计数清理了。
del x 并不一定会调用__del__
方法,只有引用计数 == 0时,__del__
()才会被执行,如果一个Python对象定义了__del__
这个方法, Python的垃圾回收机制即使发现该对象不可到达 也不会释放他. 原因是__del__
这个方式是当一个Python对象引用计数为0即将被删除前调用用来做清理工作的.由于垃圾回收找到的需要释放的对象中往往存在循环引用的情况, 对于循环引用的对象a和b, 应该先调用哪 一个对象的__del__
是无法决定的,当执行垃圾回收的时候,会将循环引用中定义了__del__
函数的类实例放到gc.garbage列表, 因此Python垃圾回收机制就放弃释放这些对象,会造成事实上的内存泄露, 转而将这些对象保存起来, 应避免在代码中定义__del__
方法.
import time
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__方法被调用")
# 创建对象
cat = Test("猫")
cat2 = cat
del cat
del cat2
time.sleep(10)
垃圾回收时,Python不能进行其它的任务,会造成程序卡顿。频繁的垃圾回收将大大降低Python的工作效率。当Python运行时,会记录其中分配对象和取消分配对象的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。
>>> import gc
>>> print(gc.get_threshold())
(700, 10, 10)
>>>
700是垃圾回收启动的阈值,后面两个10和分代回收有关,也就是新增对象与释放对象的差值为700时,进行一次垃圾回收,主要目标是循环引用,这个时候会造成卡顿
gc.enable(); gc.disable(); gc.isenabled() #开启gc(默认);关闭gc;判断gc是否开启
gc.collection() #执行一次垃圾回收,不管gc是否处于开启状态都能使用
gc.set_threshold(t0, t1, t2) #设置垃圾回收阈值;
gc.get_threshold() # 获得当前的垃圾回收阈值
gc.get_objects() #获取所有被垃圾回收器监控管理的对象
gc.get_referents(obj) #返回obj对象直接指向的对象
gc.get_referrers(obj) #返回所有直接指向obj的对象
分代回收
同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象
Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。get_threshold()返回的(700, 10, 10)返回的两个10。也就是说,每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收。理论上,存活时间久的对象,使用的越多,越不容易被回收,这也是分代回收设计的思想。
内存泄漏
本文是在python2环境下
发生内存泄漏的两中情况:
第一是对象被另一个生命周期特别长的对象所引用
第二是循环引用中的对象定义了_del_函数
检测内存泄漏的工具有很多,这里列举几种常见且有用的工具:
- objgraph python2,3下都可使用
- tracemalloc python3下使用
- pympler
objgraph
文档地址:https://mg.pov.lt/objgraph/
# dot t.dot -T png -o pic.png
count(typename) #返回该类型对象的数目,其实就是通过gc.get_objects()拿到所用的对象,然后统计指定类型的数目。
by_type(typename) #返回该类型的对象列表。线上项目,可以用这个函数很方便找到一个单例对象
show_most_common_types(limits = 10)# 打印实例最多的前N(limits)个对象,调用前,最好先gc.collet一下
show_backrefs() #生成有关objs的引用图,看出看出对象为什么不释放。
find_backref_chain(obj, predicate, max_depth=20, extra_ignore=()) #找到一条指向obj对象的最短路径,且路径的头部节点需要满足predicate函数 (返回值为True)可以快捷、清晰指出 对象的被引用的情况,后面会展示这个函数的威力
show_chain() # 将find_backref_chain 找到的路径画出来。
show_growth 可以看出自上次调用后,对象的增长情况
# -*- coding:utf-8 -*-
import time,gc,objgraph
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__ is called!")
# 创建对象
class L(list):
def __del__(self):
print('list')
def leak():
objgraph.show_growth()
a = Test("a")
b = Test("b")
# c=a
a.attrb = b
b.attra = a
l1 = L([1,2])
l2 = L([3,4])
l1.append(l2)
l2.append(l1)
leak()
# gc.collect()
print('----------')
objgraph.show_growth()
当定位到哪个对象存在内存泄漏,就可以用show_backrefs查看这个对象的引用链。
# -*- coding:utf-8 -*-
import time,gc,objgraph
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__ is called!")
# 创建对象
class L(list):
def __del__(self):
print('list')
def leak():
# objgraph.show_growth()
a = Test("a")
b = Test("b")
# c=a
a.attrb = b
b.attra = a
del a,b
l1 = L([1,2])
l2 = L([3,4])
l1.append(l2)
l2.append(l1)
leak()
gc.collect()
print(gc.garbage)
print('----------')
# objgraph.show_growth()
objgraph.show_backrefs(objgraph.by_type('Test')[0], max_depth = 10, filename = 'pic.png')
# objgraph.show_backrefs(objgraph.by_type('Test')[0], extra_ignore=(id(gc.garbage),), max_depth = 10, filename = 'pic.png')
上图所示,Test类的对象存在循环引用,并且无法用gc清除,因为循环引用对象定义了_del_方法。另外,可以看见gc.garbage(类型是list)也引用了这两个对象,原因在于当执行垃圾回收的时候,会将定义了del函数的类实例(被称为uncollectable object)放到gc.garbage列表,因此,也可以直接通过查看gc.garbage来找出定义了del的循环引用。在这里,通过增加extra_ignore来排除gc.garbage的影响。代码越复杂,相互之间的引用关系越多,show_backrefs越难以看懂。这个时候就可以使用show_chain和find_backref_chain
# -*- coding:utf-8 -*-
import time,gc,objgraph
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__ is called!")
# 创建对象
class L(list):
def __del__(self):
print('list')
def leak():
# objgraph.show_growth()
a = Test("a")
b = Test("b")
# c=a
a.attrb = b
b.attra = a
l1 = L([1,2])
l2 = L([3,4])
l1.append(l2)
l2.append(l1)
leak()
# gc.collect()
print('----------')
# objgraph.show_growth()
# objgraph.show_backrefs(objgraph.by_type('Test')[0], max_depth = 10, filename = 'chain.png')
objgraph.show_chain(
objgraph.find_backref_chain(
objgraph.by_type('Test')[0],
objgraph.is_proper_module
),
filename='chain.png'
)
对象定义了_del_方法,且存在循环引用,垃圾回收回收不了。
python2上述代码经测试正常,但是python3报错。难道是python3改进了?待确认,python3的del加gc回收 是否可以消除循环引用,答案是Python 3.4以后都可以自动处理。
另外,关于内存泄漏的定位,还可设置gc为debug模式,打印出不可回收对象,从而排查出可能发生内存泄漏的对象。
# -*- coding:utf-8 -*-
import time,gc,objgraph
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__ is called!")
# 创建对象
class L(list):
def __del__(self):
print('list')
def leak():
objgraph.show_growth()
a = Test("a")
b = Test("b")
# c=a
a.attrb = b
b.attra = a
del a,b
l1 = L([1,2])
l2 = L([3,4])
l1.append(l2)
l2.append(l1)
gc.set_debug(gc.DEBUG_LEAK)
# gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
leak()
print('----------')
objgraph.show_growth()
tracemalloc
tracemalloc 是python3内置库,非常轻量,可以用于追踪内存的使用情况,功能强大,用法也很简单,遗憾的是python2不支持。https://docs.python.org/3/library/tracemalloc.html
例:
import tracemalloc
tracemalloc.start() # 开始跟踪内存分配
test = [i for i in range(100000)]
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno') # lineno,逐行统计;filename,统计整个文件内存
for stat in top_stats:
print(stat)
结果:
/Users/mac/temp/makemoney_admin_flask/app/test.py:5: size=3533 KiB, count=99745, average=36 B
从结果来看,
文件第5行消耗了3533 KiB的内存。
如果想统计某段程序的内存情况,可以比较两段快照之间的内存,如下:
import tracemalloc
tracemalloc.start()
# ... start your application ...
snapshot1 = tracemalloc.take_snapshot()
test1 = [i for i in range(100000)]
test2 = [i for i in range(100000)]
# ... call the function leaking memory ...
snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print("[ Top 10 differences ]")
for stat in top_stats[:10]:
print(stat)
结果 :
/Users/mac/temp/makemoney_admin_flask/app/test.py:8: size=3532 KiB (+3532 KiB), count=99744 (+99744), average=36 B
/Users/mac/temp/makemoney_admin_flask/app/test.py:7: size=3532 KiB (+3532 KiB), count=99744 (+99744), average=36 B
/Users/mac/temp/makemoney_admin_flask/app/test.py:6: size=576 B (+576 B), count=1 (+1), average=576 B
/Users/mac/anaconda3/lib/python3.6/tracemalloc.py:387: size=96 B (+96 B), count=2 (+2), average=48 B
/Users/mac/anaconda3/lib/python3.6/tracemalloc.py:524: size=56 B (+56 B), count=1 (+1), average=56 B
/Users/mac/anaconda3/lib/python3.6/tracemalloc.py:281: size=40 B (+40 B), count=1 (+1), average=40 B
从打印结果得知,消耗内存的程序段分布,可以知道哪些代码消耗内存较大,分析具体内存泄漏的情况,非常有用。
pympler
也是一种可以排查追踪内存泄漏的工具,参考文档https://pythonhosted.org/Pympler/
# -*- coding:utf-8 -*-
from pympler import tracker
tr = tracker.SummaryTracker()
import time,gc,objgraph
class Test(object):
def __init__(self, name):
self.__name = name
def __del__(self):
print("__del__ is called!")
# 创建对象
class L(list):
def __del__(self):
print('list')
def leak():
a = Test("a")
b = Test("b")
# c=a
a.attrb = b
b.attra = a
# del a,b
l1 = L([1,2])
l2 = L([3,4])
l1.append(l2)
l2.append(l1)
# gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
tr.print_diff()
leak()
print('----------')
tr.print_diff()
结果:
types | # objects | total size
============================ | =========== | ============
list | 3539 | 362.31 KB
str | 4148 | 297.93 KB
dict | 48 | 72.38 KB
code | 267 | 33.38 KB
type | 23 | 20.30 KB
int | 327 | 7.66 KB
_sre.SRE_Pattern | 13 | 6.00 KB
set | 6 | 5.86 KB
weakref | 31 | 2.66 KB
tuple | 39 | 2.36 KB
getset_descriptor | 24 | 1.69 KB
function (__init__) | 13 | 1.52 KB
wrapper_descriptor | 17 | 1.33 KB
builtin_function_or_method | 11 | 792 B
property | 8 | 704 B
----------
types | # objects | total size
======================= | =========== | ============
list | 221 | 20.75 KB
str | 223 | 12.78 KB
dict | 2 | 560 B
<class '__main__.L | 2 | 224 B
int | 8 | 192 B
<class '__main__.Test | 2 | 128 B
主要看print('----------')之后的。
弱引用
弱引用模块是weakref可以用于消除循环引用。
# -*- coding:utf-8 -*-
import time,gc,objgraph
import weakref
class Test(object):
def __del__(self):
print("del is called")
def callback(self):
print("callback")
def leak():
t1 = Test()
t2 = Test()
t1.arrt2 = weakref.proxy(t2)
t2.arrt1 = weakref.proxy(t1)
leak()
gc.collect()
print(gc.garbage)