概述
本文档描述了如何分析 Python 应用中各部分内存使用量的方法,不含削减方法(如果你知道问题出在哪里,那你就应该知道如何解决)。
内存分析
统计分析
Python 的 tracemalloc 模块可以跟踪 Python 应用中的内存开销情况。阅读链接上的文档可以解决你所有问题。下面是上述文档的一些摘抄。
尽早开始跟踪
要追踪 Python 所分配的大部分内存块,模块应当通过将
PYTHONTRACEMALLOC
环境变量设置为1
,或是通过使用-X
tracemalloc
命令行选项来尽可能早地启动。
由于 tarcemalloc 只有在开启跟踪的情况下才会记录内存使用情况,也就是说如果你想尽可能跟踪所有内存使用情况,则需要尽早启动 tracemalloc。除了尽早在 python 脚本中调用 tracemalloc.start()
之外,你还可以通过更改启动命令行的形式来启动(比 tracemalloc.start()
更早)。
示例:
$ cat check_tracing.py
import tracemalloc
print("is tracing:", tracemalloc.is_tracing())
$ python check_tracing.py
is tracing: False
$ python -X tracemalloc check_tracing.py
is tracing: True
$ PYTHONTRACEMALLOC=1 python check_tracing.py
is tracing: True
打印统计值
摘自:例子-显示前10项
显示内存分配最多的10个文件:
import tracemalloc
tracemalloc.start()
# ... run your application ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("[ Top 10 ]")
for stat in top_stats[:10]:
print(stat)
Python测试套件的输出示例:
[ Top 10 ]
<frozen importlib._bootstrap>:716: size=4855 KiB, count=39328, average=126 B
<frozen importlib._bootstrap>:284: size=521 KiB, count=3199, average=167 B
/usr/lib/python3.4/collections/__init__.py:368: size=244 KiB, count=2315, average=108 B
/usr/lib/python3.4/unittest/case.py:381: size=185 KiB, count=779, average=243 B
/usr/lib/python3.4/unittest/case.py:402: size=154 KiB, count=378, average=416 B
/usr/lib/python3.4/abc.py:133: size=88.7 KiB, count=347, average=262 B
<frozen importlib._bootstrap>:1446: size=70.4 KiB, count=911, average=79 B
<frozen importlib._bootstrap>:1454: size=52.0 KiB, count=25, average=2131 B
<string>:5: size=49.7 KiB, count=148, average=344 B
/usr/lib/python3.4/sysconfig.py:411: size=48.0 KiB, count=1, average=48.0 KiB
我们可以看到 Python 从模块载入了 4855 KiB
数据(字节码和常量)并且 collections
模块分配了 244 KiB
来构建 namedtuple
类型。
更多选项,请参见 Snapshot.statistics()
显示差异
摘自:例子-计算差异
获取两个快照并显示差异:
import tracemalloc
tracemalloc.start()
# ... start your application ...
snapshot1 = tracemalloc.take_snapshot()
# ... 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)
运行 Python 测试套件的部分测试之前/之后的输出样例:
[ Top 10 differences ]
<frozen importlib._bootstrap>:716: size=8173 KiB (+4428 KiB), count=71332 (+39369), average=117 B
/usr/lib/python3.4/linecache.py:127: size=940 KiB (+940 KiB), count=8106 (+8106), average=119 B
/usr/lib/python3.4/unittest/case.py:571: size=298 KiB (+298 KiB), count=589 (+589), average=519 B
<frozen importlib._bootstrap>:284: size=1005 KiB (+166 KiB), count=7423 (+1526), average=139 B
/usr/lib/python3.4/mimetypes.py:217: size=112 KiB (+112 KiB), count=1334 (+1334), average=86 B
/usr/lib/python3.4/http/server.py:848: size=96.0 KiB (+96.0 KiB), count=1 (+1), average=96.0 KiB
/usr/lib/python3.4/inspect.py:1465: size=83.5 KiB (+83.5 KiB), count=109 (+109), average=784 B
/usr/lib/python3.4/unittest/mock.py:491: size=77.7 KiB (+77.7 KiB), count=143 (+143), average=557 B
/usr/lib/python3.4/urllib/parse.py:476: size=71.8 KiB (+71.8 KiB), count=969 (+969), average=76 B
/usr/lib/python3.4/contextlib.py:38: size=67.2 KiB (+67.2 KiB), count=126 (+126), average=546 B
我们可以看到 Python 已载入了 8173 KiB
模块数据(字节码和常量),并且这比测试之前,即保存前一个快照时载入的数据多出了 4428 KiB
。 类似地, linecache
模块已缓存 940 KiB
的 Python 源代码至格式回溯中,即从前一个快照开始的所有数据。
如果系统空闲内存太少,可以使用 Snapshot.dump()
方法将快照写入磁盘来离线分析快照。 然后使用 Snapshot.load()
方法重载快照。
场景
从上面的例子可以看到,tracemalloc 主要有两个场景:
- 显示或分析目前统计的内存数据。这主要用于分析应用启动相关数据,如应用启动内存用量等。
- 显示或分析两个快照之间的数据。这主要用于分析应用执行某些逻辑前后的内存变化。
从这两个场景出发,我们可以很容易使用 tracemalloc 跟踪并分析相应的内存问题。
对象分析
Pympler
Pympler 是一款适用于 Python 的内存分析工具。它可以分析 Python 对象中的内存表现。
获取对象大小
sys.getsizeof()
是 Python 内置模块 sys
提供的一个函数,用于返回一个对象的内存占用大小(以字节为单位)。这个大小指的是对象本身的开销,不包括其引用的子对象(除非是内建类型的容器)。
然而,很多时候我们分析内存使用的时候并不只是分析单个内存对象的内存使用,而是获取包括其子对象的内存信息(如获取列表及其内部元素的内存大小)。这个时候我们就可以使用 Pympler 获取相关信息:
from pympler import asizeof
obj = [1, 2, (3, 4), 'text']
print(asizeof.asizeof(obj))
包引入(package import)分析
包引入,即 import xxx
的过程,会加载硬盘中的 Python 库并导入到内存中。在这个过程中,主要的内存开销包括:
- Python 解释器开销:主要包括相关代码的加载及编译的相关开销
- 模块初始化对象开销:有些模块导入时会初始化一些内容,这些内容可能会持续贮留在内存中(取决于模块行为)
本章节中主要分析上述的第一类内存的开销,第二类内存开销见 [[#统计分析]] 章节。
原理解析
在模块导入过程中,Python 解释器相关开销主要体现在 importlib
这个模块中,其作用主要是从文件系统中加载 .py
或 .pyc
文件,并将其编译为字节码。在这个过程中,由于 Python 解释器要加载源码以及存储编译后的字节码,所以需要使用一些内存空间。这些内存使用一般统计在 <frozen importlib._bootstrap>
或 <frozen importlib._bootstrap_external>
下。
其中, <frozen importlib._bootstrap>
一般用于加载 Python 内部库,而 <frozen importlib._bootstrap_external>
一般用于加载外部库。因此,当我们想要削减内存消耗时,一般主要看 <frozen importlib._bootstrap_external>
就好了。
实际案例中的引入内存使用显示大致如下:
<frozen importlib._bootstrap_external>:672: size=12.0 MiB, count=119761, average=105 B
<frozen importlib._bootstrap>:241: size=1145 KiB, count=9932, average=118 B
<frozen importlib._bootstrap_external>:128: size=189 KiB, count=1277, average=151 B
<frozen importlib._bootstrap>:359: size=87.7 KiB, count=1247, average=72 B
<frozen importlib._bootstrap>:49: size=77.1 KiB, count=1161, average=68 B
<frozen importlib._bootstrap_external>:1599: size=66.3 KiB, count=153, average=444 B
<frozen importlib._bootstrap_external>:1043: size=59.0 KiB, count=1163, average=52 B
<frozen importlib._bootstrap_external>:1591: size=54.0 KiB, count=888, average=62 B
<frozen importlib._bootstrap_external>:758: size=39.6 KiB, count=678, average=60 B
<frozen importlib._bootstrap>:700: size=36.0 KiB, count=1, average=36.0 KiB
其中,importlib
的源码位于 /usr/lib/python3.xxx/importlib/
处。
阅读源码,不难发现,上面显示的 <frozen importlib._bootstrap_external>:672
实际上在函数 _compile_bytecode
中,起到加载并编译字节码的作用,而这些字节码最终会作为已引入模块的重要部分贮留在内存中。所以我们可以通过 sys.modules
查看其内存用量。
更详细的内容见 [[Python 模块导入原理]]。
package 内存使用量
python 所有已引入模块都存储于 sys.modules
处。我们只需要遍历并计算每个对象的大小,就知道 import 它的内存用量了,如下:
import sys
from pympler import asizeof
modules = sys.modules.copy() # copy to avoid module changes during iteration
supported_modules = {}
for k, v in modules.items():
try:
asizeof.asizeof(v)
supported_modules[k] = v
except Exception:
# Skip modules that cannot be measured
continue
memory_size_tuple = asizeof.asizesof(*supported_modules.values())
memory_usage = [(name, size) for name, size in zip(supported_modules.keys(), memory_size_tuple)]
memory_usage.sort(key=lambda x: x[1], reverse=True)
total_usage = sum(size for _, size in memory_usage)
[!hint]
你应该将需要度量的对象一次性传入到asizesof
中,而不是自己遍历并使用asizeof
逐个计算——后者会导致一些对象被重复计算。