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 行上。 如果加载新模块,则回溯可能会更改。

漂亮的 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()

如果 tracemalloc 模块正在跟踪 Python 内存分配,则返回 True;否则返回 False

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

tracemalloc.start(nframe: int = 1)

开始跟踪 Python 内存分配:在 Python 内存分配器上安装钩子。收集的跟踪的回溯将限制为 *nframe* 帧。默认情况下,内存块的跟踪仅存储最近的帧:限制为 1。 *nframe* 必须大于或等于 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)。只读属性。

过滤器

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

根据内存块的跟踪记录进行筛选。

关于 filename_pattern 的语法,请参阅 fnmatch.fnmatch() 函数。'.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 属性。

class tracemalloc.Frame

回溯的帧。

Traceback 类是 Frame 实例的序列。

filename

文件名 (str)。

lineno

行号 (int)。

快照

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 序列。filters 是一个 DomainFilterFilter 实例的列表。如果 filters 是一个空列表,则返回一个新的 Snapshot 实例,其中包含 traces 的副本。

所有包含性过滤器会一次性应用,如果某个 trace 不符合任何包含性过滤器,则会被忽略。如果至少有一个排除性过滤器匹配某个 trace,则该 trace 会被忽略。

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

classmethod load(filename)

从文件中加载快照。

另请参阅 dump()

statistics(key_type: str, cumulative: bool = False)

获取按 key_type 分组的 Statistic 实例的排序列表形式的统计信息。

key_type

描述

'filename'

filename

'lineno'

文件名和行号

'traceback'

traceback

如果 cumulativeTrue,则累加 trace 的回溯中所有帧的内存块大小和计数,而不仅仅是最新的帧。累积模式只能与 key_type 等于 'filename''lineno' 一起使用。

结果按照以下顺序从大到小排序:Statistic.sizeStatistic.count,然后是 Statistic.traceback

traceback_limit

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

traces

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

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

Statistic

class tracemalloc.Statistic

内存分配的统计信息。

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

另请参阅 StatisticDiff 类。

count

内存块的数量(int)。

size

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

traceback

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

StatisticDiff

class tracemalloc.StatisticDiff

旧快照实例和新快照实例之间内存分配的统计差异。

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

count

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

count_diff

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

size

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

size_diff

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

traceback

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

Trace

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 模块无法获取帧,则使用文件名为 "<unknown>",行号为 0

当获取快照时,回溯的帧数限制为 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())