tracemalloc — 跟踪内存分配 — Python 文档

来自菜鸟教程
Python/docs/3.9/library/tracemalloc
跳转至:导航、​搜索

tracemalloc — 跟踪内存分配

3.4 版中的新功能。


源代码: :source:`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 模块缓存了 Python 源代码的 940 KiB 以格式化回溯,所有这些都是自上一个快照以来的。

如果系统空闲内存很少,可以使用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 行上。 如果加载了新模块,回溯可能会发生变化。


漂亮的上衣

使用漂亮的输出显示分配最多内存的 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(...)))。


应用程序接口

功能

tracemalloc.clear_traces()

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

另见 stop()

tracemalloc.get_object_traceback(obj)

获取分配 Python 对象 obj 的回溯。 如果 tracemalloc 模块未跟踪内存分配或未跟踪对象的分配,则返回 Traceback 实例或 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() 函数。


域过滤器

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

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

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

inclusive

如果 inclusiveTrue (include),则只匹配在行号 lineno 处名称匹配 filename_pattern 的文件中分配的内存块。

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

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 序列,filtersDomainFilterFilter 实例的列表. 如果 filters 是一个空列表,则返回一个新的 Snapshot 实例和跟踪的副本。

一次应用所有包含过滤器,如果没有包含过滤器匹配它,则忽略跟踪。 如果至少有一个排他过滤器匹配一条跟踪,则该跟踪将被忽略。

3.6 版更改:DomainFilter 实例现在也被 过滤器 接受。

classmethod load(filename)

从文件加载快照。

另见 dump()

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

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

键类型

描述

'filename'

文件名

'lineno'

文件名和行号

'traceback'

追溯

如果cumulativeTrue,则累加一个trace的traceback的所有帧的内存块的大小和计数,而不仅仅是最近的帧。 累积模式只能用于 key_type 等于 'filename''lineno'

结果从大到小排序:Statistic.sizeStatistic.count,然后是Statistic.traceback

traceback_limit

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

traces

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

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


统计

class tracemalloc.Statistic

内存分配统计。

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

另请参阅 StatisticDiff 类。

count

内存块数 (int)。

size

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

traceback

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


统计差异

class tracemalloc.StatisticDiff

旧的和新的 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

内存块的地址空间(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())