python内存管理机制
具有四层结构:
内存缓冲池
的原因。对象缓冲池
机制,它基于在第二层的内存池。对于可能被经常使用、而且是immutable的对象,比如较小的整数、长度较短的字符串,python会缓存在layer3,避免频繁创建和销毁。Python有两种共存的内存管理机制: 引用计数
和垃圾回收
。python的内存回收以引用计数机制为主,引用计数
是一种非常高效的内存管理手段, 当一个Python对象被引用时其引用计数增加1, 当其不再被一个变量引用时则计数减1,当引用计数等于0时对象被删除。引用计数的优点
在于原理通俗易懂,且将对象的回收分布在代码运行时,一旦对象不再被引用,就会被释放掉(be freed),不会造成卡顿。主要缺点
是无法自动处理循环引用。
垃圾回收机制
用来弥补引用计数的不足,可回收循环引用的对象。垃圾回收机制提供了一些接口:
gc.disable() # 暂停自动垃圾回收.
gc.collect() # 执行一次完整的垃圾回收, 返回垃圾回收所找到无法到达的对象的数量.
gc.set_threshold() # 设置Python垃圾回收的阈值.
gc.set_debug() # 设置垃圾回收的调试标记. 调试信息会被写入std.err.
Python中, 所有能够引用其他对象的对象都被称为容器
(container), 因此只有容器之间才可能形成循环引用
。Python的垃圾回收机制利用了这个特点来寻找需要被释放的对象,为了记录下所有的容器对象,Python将每一个 容器都链到了一个双向链表中, 之所以使用双向链表是为了方便快速的在容器集合中插入和删除对象。有了这个维护了所有容器对象的双向链表以后, Python在垃圾回收时使用如下步骤来寻找需要释放的对象:
python中应用了分代回收机制 。简单来说就是,将存在时间短的对象容易死掉,而老年的对象不太容易死,这叫做弱代假说(weak generation hypothesis),这也很好理解,一般生命周期长的对象往往是全局变量,而短的多为局部变量或者临时定义的变量。那么,我们把当前的对象作为第0代,我们每当allocation比deallocation多到某个阈值时,就对这些对象做一次检查和清理,没有被清理的那些就存活下来,进入第1代,第一代检查做若干次后,对1代清理,存活下来的进入第2代,第二代也是如此。这样就实现了分代回收的操作。
通过sys.getrefcount(obj)对象可以获得一个对象的引用数目,返回值是真实引用数目加1(加1的原因是obj被当做参数传入了getrefcount函数)。
最近使用django项目分析一个50M的数据时(并不是一次全部读取到内存),内存在某一时刻突然飙升到2G,并保持到程序执行结束。以下是对内存溢出调试,进行的一些记录。
查阅资料后,发现可用于调试python内存的工具有resource、memory_profiler、objgraph、heap。
resource模块统计的内存是RSS。
使用该模块在某些函数执行后,打印进程占用的内存,发现了函数A执行后,内存从100M增加到2131880kb,为了更详细地定位到问题,我尝试使用memory_profiler。
memory_profiler统计的内存是RSS。
以下是代码示例
from memory_profiler import profile
@profile(precision=4)
def my_func():
a = [1] * (10 ** 6)
b = [2] * (2 * 10 ** 7)
del b
return a
if __name__ == '__main__':
my_func()
memory_profiler可以生成内存图,需要先安装pip3 install matplotlib:
mprof run __main__.py
mprof plot
第一个命令将进程的内存使用情况写入mprofile**.dat文件中,第二个命令将生成的文件展示为图。
我们可以看到,上面的图中,进程使用的最大内存是50M,然而resource模块在函数A中打印的该进程使用的内存为2G,数值差距很大。这是因为,如果存在子进程或者多进程情况,memory_profile只计算主进程的内存占用。需要分别使用以下方式执行:
mprof run --include-children
或
mprof run --multiprocess
也可以同时使用–include-children、–multiprocess,最后再执行
mprof plot
相同代码的执行结果变为:
奇怪的是,内存占用图显示了内存飙升,但是代码行对应的内存增减却是正常的。
>>> from memory_profiler import memory_usage
>>> mem_usage = memory_usage(-1, interval=.2, timeout=1)
>>> print(mem_usage)
[7.296875, 7.296875, 7.296875, 7.296875, 7.296875]
memory_usage(proc=-1, interval=.2, timeout=None)返回一段时间的内存值,其中proc=-1表示此进程,这里可以指定特定的进程号;interval=.2表示监控的时间间隔是0.2秒;timeout=1表示总共的时间段为1秒。那结果就返回5个值。
如果要返回一个函数的内存消耗,示例
def f(a, n=100):
import time
time.sleep(2)
b = [a] * n
time.sleep(1)
return b
from memory_profiler import memory_usage
print memory_usage((f, (2,), {'n' : int(1e6)}))
memory_profile支持调试,当你设置了内存阈值时,程序会在内存达到阈值时停下,阈值单位为MB。调试前,你需要为函数添加@profile修饰符
python -m memory_profiler --pdb-mmem=100 my_script.py
这功能暂时还未验证是否好用。如果不是使用不当,那么使用memory_profiler与resources模块的感受是,虽然memory_profiler可以显示每一行代码对应的内存增减,但是不是那么准确,函数A中调用了函数a1,我为函数a1添加了@profile(precision=4),显示的内存增减是正常的,但是我使用resources模块在函数a1前后打印内存,发现a1函数开始与结束时,内存占用相差了1G。memory_profiler是准确的,所以,memory_profiler我将会用其内存走势图评估应用的内存走势是否存在问题,resources用来定位出现问题的地方。
guppy可用于查看python对象占用的堆内存大小,但是该模块的最新版本只支持python2.7,我使用的是python3,暂时用不了。
结论来自于: http://guppy-pe.sourceforge.net/
安装
sudo apt-get install xdot
sudo pip3 install objgraph
objgraph能够通过图的形式展示对象之间的引用情况:
import objgraph
a = [1]
b = [2]
a.append(b)
b.append(a)
objgraph.show_refs([a], filename='ref_topo.png')
我最终通过memory_profile的cpu走势图与resources模块定位到了问题,出问题的具体函数为:
def get_a(content, pid, time, process):
match = re.search(
'(^----- pid ' + pid + ' at ' + time + ' -----\nCmd line: '
+ process + '\n('
'.|\n)*?----- end ' + pid + ' '
'-----)',
content, re.M)
if not match:
return None
return match.group(1)
这个函数的目的是从整个文件内容content中根据pid、time、包名获取匹配的内容。将以上函数的正则修改为:
def get_a(content, pid, time, process):
match = re.search(
'(^----- pid ' + pid + ' at ' + time + ' -----\nCmd line: '
+ process + '\n' +
'(.*\n)*?----- end ' + pid + ' '
'-----)',
content, re.M)
if not match:
return None
return match.group(1)
修改部分仅仅是将不需要指定关键字的多行从“(.|\n)?”修改为“(.\n)*?”,执行代码后,内存占用的走势变为:
最高的内存占用从2G变为250M,这与正则的匹配逻辑有关。同时,在正则前后,content对象的引用计数增加了7。更为具体的原因后续添加。由此案例我知,在正则匹配时不会慎重使用或(|)。
以上demon执行结果也说明使用”(.|\n)?”表示任意行的性能比”[\S\s]?”差很多。
参考资料:
1. 《Python内存问题:提示和技巧》
2. 《利用PyCharm的Profile工具进行Python性能分析》 –用处不大
3. 《官方文档 memory_profiler 0.52.0》
4. Python的7种性能测试工具:timeit、profile、cProfile、line_profiler、memory_profiler、PyCharm图形化性能测试工具、objgraph
5. Python内存管理机制及优化简析