一、Python内存管理
Python中,python中,一切都是对象,又分为mutable和immutable对象。二者区分的标准在于是否可以原地修改,“原地”可以理解为相同的地址。可以通过id()查看一个对象的“地址”,如果通过变量修改对象的值,但id没发生变化,那么就是mutable,否则就是immutable。比如:
#immutable
a=1
print(id(a))#140734991946784
a=3
print(id(a))#140734991946848
#mutable
b =[1,2,3]print(id(b))#1273753854536
b.append(4)print(id(b))#1273753854536
1、引用计数
引用计数(References count),指的是每个Python对象都有一个计数器,记录着当前有多少个变量指向这个对象。
通过sys.getrefcount(obj)对象可以获得一个对象的引用数目,返回值是真实引用数目加1(加1的原因是obj被当做参数传入了getrefcount函数),例如:
importsys
s= 1
print(sys.getrefcount(s))#1297
a = ‘jkl’
print(sys.getrefcount(a))#4
从中可以看出,Python中的对象缓存池会缓存十分常用的immutable对象,比如整数1。
2、垃圾回收
这里强调一下,本文中的的垃圾回收是狭义的垃圾回收,是指当出现循环引用,引用计数无计可施的时候采取的垃圾清理算法。
在python中,使用标记-清除算法(mark-sweep)和分代(generational)算法来垃圾回收
3、gc module
这里的gc(garbage collector)是Python 标准库,该module提供了与上一节“垃圾回收”内容相对应的接口。通过这个module,可以开关gc、调整垃圾回收的频率、输出调试信息。gc模块是很多其他模块(比如objgraph)封装的基础,在这里先介绍gc的核心API。
import gc
gc.enable()#开启gc(默认开启)
gc.disable()#关闭gc
gc.isenabled()#判断gc是否开启
gc.collect()#执行一次垃圾回收,不管gc是否处于开启状态都能使用
gc.set_threshold(t0,t1,t2)#设置垃圾回收阈值
gc.get_threshold()# 获得当前的垃圾回收阈值
gc.get_objects()#返回所有被垃圾回收器(collector)管理的对象
gc.get_referents(*obj)#返回obj对象直接指向的对象
gc.get_referrers(*obj)#返回所有直接指向obj的对象
gc.set_debug(gc.DEBUG_COLLECTABLE)#打印可以被垃圾回收器回收的对象
gc.set_debug(gc.DEBUG_UNCOLLECTABLE)#打印无法被垃圾回收器回收的对象,即定义了__del__的对象
gc.set_debug(gc.DEBUG_SAVEALL)#当设置了这个选项,可以被拉起回收的对象不会被真正销毁(free),而是放到gc.garbage这个列表里面,利于在线上查找问题
二、内存泄漏
内存泄漏产生的两种情况:
1、是对象被另一个生命周期特别长的对象所引用。
解决方式:只要在适当的时机解除引用。
2、是循环引用中的对象定义了__del__函数,如果定义了__del__函数,那么Python中的解析器就
不能判断解析对象,所以不会做处理。
解决方法:要么不再使用__del__函数,换一种实现方式,要么解决循环引用。
查找内存泄漏的两个库:gc、objgraph
1、objgraph
objgraph的实现调用了gc的这几个函数:gc.get_objects(), gc.get_referents(), gc.get_referers(),然后构造出对象之间的引用关系。
常用的方法:
1、def count(typename):返回该类型对象的数目,其实就是通过gc.get_objects()拿到所用的对象,然后统计指定类型的数目。
2、def by_type(typename):返回该类型的对象列表。线上项目,可以用这个函数很方便找到一个单例对象。
3、def show_most_common_types(limits = 10):打印实例最多的前N(limits)个对象,这个函数非常有用。在《Python内存优化》一文中也提到,该函数能发现可以用slots进行内存优化的对象。
3、def show_growth():统计自上次调用以来增加得最多的对象,这个函数非常有利于发现潜在的内存泄露。函数内部调用了gc.collect(),因此即使有循环引用也不会对判断造成影响。
4、def show_backrefs():生产一张有关objs的引用图,看出看出对象为什么不释放,后面会利用这个API来查内存泄露
5、def find_backref_chain(obj, predicate, max_depth=20, extra_ignore=()):找到一条指向obj对象的最短路径,且路径的头部节点需要满足predicate函数 (返回值为True),可以快捷、清晰指出 对象的被引用的情况,后面会展示这个函数的威力
6、def show_chain():将find_backref_chain 找到的路径画出来, 该函数事实上调用show_backrefs,只是排除了所有不在路径中的节点。
2、查找内存泄漏
查找方法,先调用一次objgraph.show_growth(),然后调用怀疑内存泄漏的函数,最后再调用一次objgraph.show_growth(),看看是否有增加对象。
#-*- coding: utf-8 -*-
importobjgraph
da=[]classa():pass
defrun():
b=a()
da.append(b)ifTrue:returnda.remove(b)if __name__ == ‘__main__’:
objgraph.show_growth()try:
run()except:passobjgraph.show_growth()#显示#function 2150 +2150#wrapper_descriptor 1096 +1096#dict 1037 +1037#tuple 851 +851#builtin_function_or_method 763 +763#method_descriptor 753 +753#weakref 711 +711#getset_descriptor 393 +393#member_descriptor 307 +307#type 306 +306#a 1 +1
三、循环引用
如果存在循环引用,那么Python的gc就必须开启(gc.isenabled()返回True),否则就会内存泄露。
1、定位循环引用
这里还是是用GC模块和objgraph来定位循环引用。需要注意的事,一定要先禁用gc(调用gc.disable()), 防止误差。
这里利用之前介绍循环引用时使用过的例子: a, b两个OBJ对象形成循环引用
importobjgraph,gcclassA(object):pass
defrun():
a,b=A(),A()
a.attr_b=b
b.attr_a=aif __name__ == ‘__main__’:
gc.disable()for x in range(50):
run()
objgraph.show_most_common_types(20)#function 2150#dict 1125#wrapper_descriptor 1092#builtin_function_or_method 762#method_descriptor 752#tuple 670#weakref 597#getset_descriptor 375#member_descriptor 307#type 193#cell 173#list 152#module 112#ModuleSpec 110#A 100#classmethod 86#set 79#_NamedIntConstant 74#frozenset 72#SourceFileLoader 70
在实际项目中,不大可能到处用objgraph.show_most_common_types或者objgraph.by_type来排查循环引用,效率太低。有没有更好的办法呢,有的,那就是使用gc模块的debug 选项。在前面介绍gc模块的时候,就介绍了 # gc: collectable # gc: collectable # gc: collectable ” v:shapes=”_x0000_s1029″>gc.DEBUG_COLLECTABLE 选项,我们来试试。
importgc,timeclassA(object):pass
defrun():
a, b=A(), A()
a.attr_b=b
b.attr_a=aif __name__ == ‘__main__’:
gc.disable()
gc.set_debug(gc.DEBUG_COLLECTABLE)for x in range(1):
run()
gc.collect()
time.sleep(1)#gc: collectable #gc: collectable #gc: collectable #gc: collectable
2、消灭循环引用
找到循环引用关系之后,解除循环引用就不是太难的事情,总的来说,有两种办法:
①手动解除与使用weakref。
②手动解除很好理解,就是在合适的时机,解除引用关系
常用的方法:
(1)weakref.ref(object, callback = None)
创建一个对object的弱引用,返回值为weakref对象,callback: 当object被删除的时候,会调用callback函数,在标准库logging (__init__.py)中有使用范例。使用的时候要用()解引用,如果referant已经被删除,那么返回None。比如下面的例子
importweakrefclassA(object):deff(self):print(“asd”)if __name__ == ‘__main__’:
a=A()
w=weakref.ref(a)
w().f()dela
w().f()
运行上面的代码,会抛出异常:AttributeError: ‘NoneType’ object has no attribute ‘f’。因为这个时候被引用的对象已经被删除了
(2)weakref.proxy(object, callback = None)
创建一个代理,返回值是一个weakproxy对象,callback的作用同上。使用的时候直接用 和object一样,如果object已经被删除 那么跑出异常 ReferenceError: weakly-referenced object no longer exists。
importweakrefclassA(object):deff(self):print(“asd”)if __name__ == ‘__main__’:
a=A()
w=weakref.proxy(a)
w.f()dela
w.f()
(3)weakref.WeakSet
这个是一个弱引用集合,当WeakSet中的元素被回收的时候,会自动从WeakSet中删除。WeakSet的实现使用了weakref.ref,当对象加入WeakSet的时候,使用weakref.ref封装,指定的callback函数就是从WeakSet中删除。感兴趣的话可以直接看源码(_weakrefset.py),下面给出一个参考例子:
importweakrefclassA(object):deff(self):print(“asd”)if __name__ == ‘__main__’:
a=A()
w=weakref.WeakSet()
w.add(a)print (len(w))#1
delaprint (len(w))#0
Python中objgraph模块官方文档网址:https://mg.pov.lt/objgraph/
Python中的gc模块官方文档网址:https://docs.python.org/3/library/gc.html