tracemalloc — 追踪内存分配

在 3.4 版本中添加。

源代码: Lib/tracemalloc.py


tracemalloc 模块是一个调试工具,用于追踪 Python 分配的内存块。它提供以下信息

  • 分配对象的回溯

  • 按文件名和行号统计已分配的内存块:已分配内存块的总大小、数量和平均大小

  • 计算两个快照之间的差异以检测内存泄漏

要追踪 Python 分配的大多数内存块,应通过将 PYTHONTRACEMALLOC 环境变量设置为 1,或通过使用 -X tracemalloc 命令行选项,尽可能早地启动该模块。可以在运行时调用 tracemalloc.start() 函数以开始追踪 Python 内存分配。

默认情况下,已分配内存块的追踪仅存储最近的帧(1 个帧)。要在启动时存储 25 个帧:将 PYTHONTRACEMALLOC 环境变量设置为 25,或使用 -X tracemalloc=25 命令行选项。

示例

显示前 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() 方法重新加载快照。

获取内存块的回溯

显示最大内存块的回溯的代码

import tracemalloc

# Store 25 frames
tracemalloc.start(25)

# ... run your application ...

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('traceback')

# pick the biggest memory block
stat = top_stats[0]
print("%s memory blocks: %.1f KiB" % (stat.count, stat.size / 1024))
for line in stat.traceback.format():
    print(line)

Python 测试套件的输出示例(回溯限制为 25 帧)

903 memory blocks: 870.1 KiB
  File "<frozen importlib._bootstrap>", line 716
  File "<frozen importlib._bootstrap>", line 1036
  File "<frozen importlib._bootstrap>", line 934
  File "<frozen importlib._bootstrap>", line 1068
  File "<frozen importlib._bootstrap>", line 619
  File "<frozen importlib._bootstrap>", line 1581
  File "<frozen importlib._bootstrap>", line 1614
  File "/usr/lib/python3.4/doctest.py", line 101
    import pdb
  File "<frozen importlib._bootstrap>", line 284
  File "<frozen importlib._bootstrap>", line 938
  File "<frozen importlib._bootstrap>", line 1068
  File "<frozen importlib._bootstrap>", line 619
  File "<frozen importlib._bootstrap>", line 1581
  File "<frozen importlib._bootstrap>", line 1614
  File "/usr/lib/python3.4/test/support/__init__.py", line 1728
    import doctest
  File "/usr/lib/python3.4/test/test_pickletools.py", line 21
    support.run_doctest(pickletools)
  File "/usr/lib/python3.4/test/regrtest.py", line 1276
    test_runner()
  File "/usr/lib/python3.4/test/regrtest.py", line 976
    display_failure=not verbose)
  File "/usr/lib/python3.4/test/regrtest.py", line 761
    match_tests=ns.match_tests)
  File "/usr/lib/python3.4/test/regrtest.py", line 1563
    main()
  File "/usr/lib/python3.4/test/__main__.py", line 3
    regrtest.main_in_temp_cwd()
  File "/usr/lib/python3.4/runpy.py", line 73
    exec(code, run_globals)
  File "/usr/lib/python3.4/runpy.py", line 160
    "__main__", fname, loader, pkg_name)

我们可以看到,在 importlib 模块中分配了最多的内存来从模块中加载数据(字节码和常量):870.1 KiB。回溯是 importlib 最近加载数据的位置:在 doctest 模块的 import pdb 行上。如果加载了新模块,回溯可能会发生变化。

Pretty top

代码用于显示分配最多内存的 10 行,并以美观的方式输出,忽略 <frozen importlib._bootstrap><unknown> 文件

import linecache
import os
import tracemalloc

def display_top(snapshot, key_type='lineno', limit=10):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        print("#%s: %s:%s: %.1f KiB"
              % (index, frame.filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))

tracemalloc.start()

# ... run your application ...

snapshot = tracemalloc.take_snapshot()
display_top(snapshot)

Python 测试套件输出示例

Top 10 lines
#1: Lib/base64.py:414: 419.8 KiB
    _b85chars2 = [(a + b) for a in _b85chars for b in _b85chars]
#2: Lib/base64.py:306: 419.8 KiB
    _a85chars2 = [(a + b) for a in _a85chars for b in _a85chars]
#3: collections/__init__.py:368: 293.6 KiB
    exec(class_definition, namespace)
#4: Lib/abc.py:133: 115.2 KiB
    cls = super().__new__(mcls, name, bases, namespace)
#5: unittest/case.py:574: 103.1 KiB
    testMethod()
#6: Lib/linecache.py:127: 95.4 KiB
    lines = fp.readlines()
#7: urllib/parse.py:476: 71.8 KiB
    for a in _hexdig for b in _hexdig}
#8: <string>:5: 62.0 KiB
#9: Lib/_weakrefset.py:37: 60.0 KiB
    self.data = set()
#10: Lib/base64.py:142: 59.8 KiB
    _b32tab2 = [a + b for a in _b32tab for b in _b32tab]
6220 other: 3602.8 KiB
Total allocated size: 5303.1 KiB

有关更多选项,请参见 Snapshot.statistics()

记录所有已跟踪内存块的当前大小和峰值大小

以下代码通过创建这些数字的列表来低效地计算类似 0 + 1 + 2 + ... 的两个和。此列表会暂时占用大量内存。我们可以使用 get_traced_memory()reset_peak() 来观察在计算和后的小内存使用情况以及在计算期间的峰值内存使用情况

import tracemalloc

tracemalloc.start()

# Example code: compute a sum with a large temporary list
large_sum = sum(list(range(100000)))

first_size, first_peak = tracemalloc.get_traced_memory()

tracemalloc.reset_peak()

# Example code: compute a sum with a small temporary list
small_sum = sum(list(range(1000)))

second_size, second_peak = tracemalloc.get_traced_memory()

print(f"{first_size=}, {first_peak=}")
print(f"{second_size=}, {second_peak=}")

输出

first_size=664, first_peak=3592984
second_size=804, second_peak=29704

使用 reset_peak() 确保我们能够准确地记录在计算 small_sum 期间的峰值,即使它远小于自 start() 调用以来内存块的整体峰值大小。如果没有调用 reset_peak()second_peak 仍然是计算 large_sum 的峰值(即等于 first_peak)。在这种情况下,两个峰值都远高于最终内存使用量,这表明我们可以进行优化(通过删除对 list 的不必要调用,并编写 sum(range(...)))。

API

函数

tracemalloc.clear_traces()

清除 Python 分配的内存块的痕迹。

另请参见 stop()

tracemalloc.get_object_traceback(obj)

获取 Python 对象 obj 被分配时的回溯。返回一个 Traceback 实例,或者如果 tracemalloc 模块未跟踪内存分配或未跟踪对象的分配,则返回 None

另请参阅 gc.get_referrers()sys.getsizeof() 函数。

tracemalloc.get_traceback_limit()

获取跟踪回溯中存储的最大帧数。

tracemalloc 模块必须跟踪内存分配才能获取限制,否则会引发异常。

限制由 start() 函数设置。

tracemalloc.get_traced_memory()

获取 tracemalloc 模块跟踪的内存块的当前大小和峰值大小,形式为元组:(current: int, peak: int)

tracemalloc.reset_peak()

tracemalloc 模块跟踪的内存块的峰值大小设置为当前大小。

如果 tracemalloc 模块未跟踪内存分配,则不执行任何操作。

此函数仅修改记录的峰值大小,并且不会修改或清除任何跟踪,这与 clear_traces() 不同。在调用 reset_peak() 之前使用 take_snapshot() 拍摄的快照可以与调用之后拍摄的快照进行有意义的比较。

另请参阅 get_traced_memory()

已在版本 3.9 中添加。

tracemalloc.get_tracemalloc_memory()

获取 tracemalloc 模块用于存储内存块追踪记录的内存使用情况(以字节为单位)。返回一个 int

tracemalloc.is_tracing()

True 如果 tracemalloc 模块正在追踪 Python 内存分配,否则为 False

另请参见 start()stop() 函数。

tracemalloc.start(nframe: int = 1)

开始追踪 Python 内存分配:在 Python 内存分配器上安装钩子。收集的追踪记录的追踪将限制为 nframe 帧。默认情况下,内存块的追踪仅存储最近的帧:限制为 1nframe 必须大于或等于 1

您仍然可以通过查看 Traceback.total_nframe 属性来读取组成追踪记录的原始总帧数。

存储多于 1 帧仅对按 'traceback' 分组计算统计信息或计算累积统计信息有用:请参见 Snapshot.compare_to()Snapshot.statistics() 方法。

存储更多帧会增加 tracemalloc 模块的内存和 CPU 开销。使用 get_tracemalloc_memory() 函数来测量 tracemalloc 模块使用了多少内存。

环境变量 PYTHONTRACEMALLOC (PYTHONTRACEMALLOC=NFRAME) 和命令行选项 -X tracemalloc=NFRAME 可用于在启动时开始跟踪。

另请参见 stop()is_tracing()get_traceback_limit() 函数。

tracemalloc.stop()

停止跟踪 Python 内存分配:卸载 Python 内存分配器上的挂钩。还会清除 Python 分配的所有先前收集的内存块跟踪。

调用 take_snapshot() 函数在清除跟踪之前对其进行快照。

另请参见 start()is_tracing()clear_traces() 函数。

tracemalloc.take_snapshot()

对 Python 分配的内存块跟踪进行快照。返回一个新的 Snapshot 实例。

快照不包括在 tracemalloc 模块开始跟踪内存分配之前分配的内存块。

跟踪的回溯限制为 get_traceback_limit() 帧。使用 start() 函数的 nframe 参数存储更多帧。

要获取快照,tracemalloc 模块必须跟踪内存分配,请参阅 start() 函数。

另请参阅 get_object_traceback() 函数。

DomainFilter

class tracemalloc.DomainFilter(inclusive: bool, domain: int)

按地址空间(域)过滤内存块的跟踪。

在版本 3.6 中添加。

inclusive

如果 inclusiveTrue(包含),则匹配在地址空间 domain 中分配的内存块。

如果 inclusiveFalse(排除),则匹配未在地址空间 domain 中分配的内存块。

domain

内存块的地址空间(int)。只读属性。

Filter

class tracemalloc.Filter(inclusive: bool, filename_pattern: str, lineno: int = None, all_frames: bool = False, domain: int = None)

过滤内存块的跟踪。

请参阅 fnmatch.fnmatch() 函数,了解 filename_pattern 的语法。 '.pyc' 文件扩展名已替换为 '.py'

示例

  • Filter(True, subprocess.__file__) 仅包括 subprocess 模块的跟踪

  • Filter(False, tracemalloc.__file__) 排除 tracemalloc 模块的跟踪

  • Filter(False, "<unknown>") 排除空回溯

在版本 3.5 中更改: '.pyo' 文件扩展名不再替换为 '.py'

在版本 3.6 中更改: 添加了 domain 属性。

domain

内存块的地址空间(intNone)。

tracemalloc 使用域 0 来跟踪 Python 分配的内存。C 扩展可以使用其他域来跟踪其他资源。

inclusive

如果 inclusiveTrue(包含),则仅匹配在文件名匹配 filename_pattern 且行号为 lineno 的文件中分配的内存块。

如果 inclusiveFalse(排除),则忽略在文件名匹配 filename_pattern 且行号为 lineno 的文件中分配的内存块。

lineno

过滤器的行号(int)。如果 linenoNone,则过滤器匹配任何行号。

filename_pattern

过滤器的文件名模式(str)。只读属性。

all_frames

如果 all_framesTrue,则检查回溯的所有帧。如果 all_framesFalse,则仅检查最近的帧。

如果回溯限制为 1,则此属性无效。请参阅 get_traceback_limit() 函数和 Snapshot.traceback_limit 属性。

Frame

class tracemalloc.Frame

回溯的帧。

Traceback 类是 Frame 实例的序列。

filename

文件名 (str)。

lineno

行号 (int)。

Snapshot

class tracemalloc.Snapshot

Python 分配的内存块的跟踪快照。

take_snapshot() 函数创建一个快照实例。

compare_to(old_snapshot: Snapshot, key_type: str, cumulative: bool = False)

计算与旧快照的差异。获取统计信息,作为按key_type分组的 StatisticDiff 实例的排序列表。

有关key_typecumulative 参数,请参见 Snapshot.statistics() 方法。

结果按以下方式从大到小排序:StatisticDiff.size_diff 的绝对值、StatisticDiff.sizeStatisticDiff.count_diff 的绝对值、Statistic.count,然后按 StatisticDiff.traceback 排序。

dump(filename)

将快照写入文件。

使用 load() 重新加载快照。

filter_traces(filters)

创建一个新的 Snapshot 实例,其中包含经过筛选的 traces 序列,filtersDomainFilterFilter 实例的列表。如果filters 是一个空列表,则返回一个新的 Snapshot 实例,其中包含跟踪的副本。

所有包含性过滤器一次应用,如果没有任何包含性过滤器与跟踪匹配,则忽略该跟踪。如果至少有一个排他性过滤器与跟踪匹配,则忽略该跟踪。

在版本 3.6 中更改:filters 中现在也接受 DomainFilter 实例。

类方法 加载(文件名)

从文件中加载快照。

另请参见 转储()

统计(键类型: str, 累积: bool = False)

键类型分组获取统计信息,作为 Statistic 实例的排序列表

键类型

描述

'文件名'

文件名

'行号'

文件名和行号

'回溯'

回溯

如果累积True,则累积跟踪回溯的所有帧的内存块的大小和计数,而不仅仅是最新的帧。累积模式只能与键类型等于 'filename''lineno' 一起使用。

结果按以下方式从大到小排序: Statistic.sizeStatistic.count,然后按 Statistic.traceback 排序。

回溯限制

存储在 traces 的回溯中的最大帧数:在获取快照时 get_traceback_limit() 的结果。

跟踪

Python 分配的所有内存块的跟踪:Trace 实例的序列。

序列的顺序未定义。使用 Snapshot.statistics() 方法获取已排序的统计信息列表。

统计信息

class tracemalloc.Statistic

内存分配的统计信息。

Snapshot.statistics() 返回 Statistic 实例的列表。

另请参阅 StatisticDiff 类。

count

内存块数(int)。

size

内存块的总大小(以字节为单位)(int)。

traceback

分配内存块的回溯,Traceback 实例。

统计信息差异

class tracemalloc.StatisticDiff

Snapshot 实例和新 Snapshot 实例之间内存分配的统计信息差异。

Snapshot.compare_to() 返回 StatisticDiff 实例的列表。另请参阅 Statistic 类。

count

新快照中的内存块数(int):如果新快照中已释放内存块,则为 0

count_diff

新旧快照之间的内存块数量差异(int):如果内存块已在新的快照中分配,则为 0

size

新快照中内存块的总大小(以字节为单位)(int):如果内存块已在新快照中释放,则为 0

size_diff

新旧快照之间内存块总大小的差异(以字节为单位)(int):如果内存块已在新快照中分配,则为 0

traceback

分配内存块的回溯,Traceback 实例。

跟踪

class tracemalloc.Trace

内存块的跟踪。

Snapshot.traces 属性是 Trace 实例的序列。

3.6 版中已更改:添加了 domain 属性。

domain

内存块的地址空间(int)。只读属性。

tracemalloc 使用域 0 来跟踪 Python 分配的内存。C 扩展可以使用其他域来跟踪其他资源。

size

内存块的大小(以字节为单位)(int)。

traceback

分配内存块的回溯,Traceback 实例。

回溯

class tracemalloc.Traceback

从最旧帧到最新帧排序的 Frame 实例序列。

回溯至少包含 1 帧。如果 tracemalloc 模块未能获取帧,则使用行号 0 处的文件名 "<unknown>"

当拍摄快照时,跟踪的回溯被限制为 get_traceback_limit() 帧。请参阅 take_snapshot() 函数。回溯的原始帧数存储在 Traceback.total_nframe 属性中。这允许了解回溯是否因回溯限制而被截断。

Trace.traceback 属性是 Traceback 实例的实例。

3.7 版中已更改:帧现在从最旧到最新排序,而不是从最新到最旧排序。

total_nframe

截断前组成回溯的帧总数。如果信息不可用,可以将此属性设置为 None

3.9 版中已更改:添加了 Traceback.total_nframe 属性。

format(limit=None, most_recent_first=False)

将回溯格式化为行列表。使用 linecache 模块从源代码中检索行。如果设置了 limit,则在 limit 为正数时格式化最新 limit 帧。否则,格式化 abs(limit) 最旧帧。如果 most_recent_firstTrue,则格式化帧的顺序将被反转,返回最新帧,而不是最后一个帧。

类似于 traceback.format_tb() 函数,但 format() 不包括换行符。

示例

print("Traceback (most recent call first):")
for line in traceback:
    print(line)

输出

Traceback (most recent call first):
  File "test.py", line 9
    obj = Object()
  File "test.py", line 12
    tb = tracemalloc.get_object_traceback(f())