python之内存调试

标签: python  高内存  分析

python内存管理知识基础

python内存管理机制具有四层结构:

  1. layer 0:最底层(0层)是C运行的malloc和free接口,往上的三层才是由Python实现并且维护的。
  2. layer 1:第1层则是在第0层的基础之上对其提供的接口进行了统一的封装。
  3. layer 2:Python为了避免频繁的申请和删除内存所造成系统切换于用户态和核心态的开销,在第2层引入了内存池机制,专门用来管理小内存的申请和释放。在Linux上运行过Python服务器的程序都知道,python不会立即将释放的内存归还给操作系统,这就是内存缓冲池的原因。
  4. layer 3:第3层主要是对象缓冲池机制,它基于在第二层的内存池。对于可能被经常使用、而且是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在垃圾回收时使用如下步骤来寻找需要释放的对象:

  1. 对于每一个容器对象, 设置一个gc_refs值, 并将其初始化为该对象的引用计数值.
  2. 对于每一个容器对象, 找到所有其引用的对象, 将被引用对象的gc_refs值减1.
  3. 执行完步骤2以后所有gc_refs值还大于0的对象都被非容器对象引用着, 至少存在一个非循环引用。 因此 不能释放这些对象,将他们放入另一个集合。
  4. 在步骤3中不能被释放的对象, 如果他们引用着某个对象,被引用的对象也是不能被释放的, 因此将这些对象也放入另一个集合中。
  5. 此时还剩下的对象都是无法到达的对象. 现在可以释放这些对象了.

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。

1. resource

  • 使用:print(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss))
  • 效果:显示当前进程使用了多少内存,resource.RUSAGE_SELF表示只将自己计算在内,不包括当前进程的子进程,RUSAGE_BOTH相当于当前进程和子进程自己的总和(与平台相关)。不同平台上ru_maxrss的值的单位是不一样的,在OS X上单位是Byte,但是在Linux上单位是KB。

resource模块统计的内存是RSS。

使用该模块在某些函数执行后,打印进程占用的内存,发现了函数A执行后,内存从100M增加到2131880kb,为了更详细地定位到问题,我尝试使用memory_profiler。

2. memory_profiler

2.1 简单使用

memory_profiler统计的内存是RSS。

  • 安装: pip3 install -U memory_profiler,为了提高效率,另外还应该安装pip3 install psutil
  • 使用:用@profile修饰需要查看内存的函数
  • 执行:python main.py
  • 效果:显示被@profile的函数中每一行代码对应的内存增减

以下是代码示例

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()

2.2 生成与时间相对应的内存变化图

memory_profiler可以生成内存图,需要先安装pip3 install matplotlib:

 mprof run __main__.py 
 mprof plot

第一个命令将进程的内存使用情况写入mprofile**.dat文件中,第二个命令将生成的文件展示为图。

这里写图片描述

我们可以看到,上面的图中,进程使用的最大内存是50M,然而resource模块在函数A中打印的该进程使用的内存为2G,数值差距很大。这是因为,如果存在子进程或者多进程情况,memory_profile只计算主进程的内存占用。需要分别使用以下方式执行:

mprof run --include-children <script>
或
mprof run --multiprocess <script>

也可以同时使用–include-children、–multiprocess,最后再执行

mprof plot

相同代码的执行结果变为:

这里写图片描述

奇怪的是,内存占用图显示了内存飙升,但是代码行对应的内存增减却是正常的。

2.3 memory_profiler支持api调用

>>> 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)}))

2.4 调试

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用来定位出现问题的地方。

3. heap

guppy可用于查看python对象占用的堆内存大小,但是该模块的最新版本只支持python2.7,我使用的是python3,暂时用不了。
结论来自于: http://guppy-pe.sourceforge.net/

4. objgraph

安装

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')

5. 问题定位

我最终通过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。更为具体的原因后续添加。由此案例我知,在正则匹配时不会慎重使用或(|)。

6. 收获

这里写图片描述

以上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内存管理机制及优化简析

版权声明:本文为lj1404536198原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lj1404536198/article/details/80900549

智能推荐

CentOS 7配置南大docker镜像

文章目录 CentOS 7配置南大docker镜像 0.帮助页面 1.系统要求 2.卸载旧版本(没有旧版本可跳过) 3.安装方式 4.准备工作 5.可选操作 Stable Test Nightly 6.安装docker引擎 7. (可选)修改配置文件防止与xshell连接冲突 8.启动docker CentOS 7配置南大docker镜像 0.帮助页面 南大docker源:https://mirr...

Qcon演讲纪实:详解如何在实时视频通话中实现AR功能

2018年4月20日-22日,由 infoQ 主办的 Qcon 2018全球软件开发大会在北京如期举行。声网首席 iOS 研发工程师,iOS 端移动应用产品设计和技术架构负责人龚宇华,受邀分享了《基于 ARkit 和 ARcore,在实时视频通话中实现 AR 功能》,在演讲中剖析了 AR 与 VR 差异,ARKit 的工作原理,以及逐步讲解如何基于 ARKit 与声网Agora SDK 创建 AR...

POJ2348 UVa10368 HDU1525 Euclid's Game【博弈】

Euclid's GameTime Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submission(s): 4106    Accepted Submission(s): 1947 Probl...

使用Breeze.js编写更好的查询

这篇文章是由同行评审Agbonghama柯林斯 。 感谢所有SitePoint的审稿作出SitePoint内容也可以是最好的! 数据量正在迅速发展,他们正在变得越来越复杂,维护。 许多开发人员希望避免由数据问题他们的工作过程中造成的问题和头痛。 一个使我们的工作更轻松的图书馆是Breeze.js 。 在这篇文章中,我们将讨论我们如何能够写出更好的查询与Breeze.js。 但是首先,我们应该知道什...

Netty框架构建Nio编程

~~~ 随手点赞,养成习惯 ~~~ 为什么选择Netty框架 Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的。 优点: ① API使用简单,开发门槛低 ②功能强大,预置了多种编解码功能,支持多种主流协议 ③ 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展; ④性能高,通过与其他业界主流的NIO框架对比,Nett...

猜你喜欢

【JZOJ5262】【GDOI2018模拟8.12】树(DP,性质题)

Description Solution 首先我们可以知道两个性质:1、路径u-v和路径v-w可以合并为路径u-w;2、路径u1-v1加路径u2-v2和路径u1-v2加路径u2-v1是等价的(就是起始点和终点可以互换) 那么知道这些性质之后就很好做了。我们只用知道每个点多少次做起点和多少次做终点。 我们设f[i]表示满足i子树的需求i上的值要是多少。 那么枚举i的所有儿子,判断a[i]-f[i],...

【String-easy】541. Reverse String II 反转的元素,有反转个数和间隔

1. 题目原址 https://leetcode.com/problems/reverse-string-ii/ 2. 题目描述 3. 题目大意 给定一个字符串,和字符串的间隔k, 这个k表示每k个数反转一次,然后再间隔k个元素再反转k个元素。 4. 解题思路 只要按照间隔去反转就可以了。然后间隔k个元素不反转是通过让i每次递增 2*k完成的。 5. AC代码 6. 相似题型 【1】344. Re...

【C语言笔记结构体】

我们都知道C语言中变量的类型决定了变量存储占用的空间。当我们要使用一个变量保存年龄时可以将其声明为int类型,当我们要使用一个变量保存某一科目的考试成绩时可以将其声明为float。 那么,当我们要做一个学生信息管理系统时,需要保存学生的姓名、学号、年龄等信息,该怎么做呢? 如当要保存三个学生的信息时, 方法一是: 方法二是: 显然,方法二跟更清晰,因为它把name、num、age都集成在一个模板,...

39. Combination Sum 回溯算法简析

LeetCode传送门     这道题要求给你一组正数 C,然后给你一个目标数 T,让你从那组C中找到加在一起等于 T 的那些组合。     例如:给你 [2,3,6,7] 和 7,则返回 [[2,2,3],[7] ] 。      想解决这个问题前,我们首先引入一个新问题,图(树)的遍历问题。  ...

git安装|Linux系统安装 git|Linux如何安装git?Linux通过远程安装git|

Git是一个开源的分布式版本控制系统,可以有效、高速地处理项目版本管理。Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。 开发者需要一个GIT账号,通过这个查看项目的提交记录,可以更加清楚项目的开发情况,便于版本控制。 以下介绍在CentOS8操作系统搭建GIT服务器。   一、安装GIT服务器流程   安装GIT...