
CPython tracemalloc 模块完全指南内存分配追踪、快照对比与泄漏定位【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpythonCPython 的tracemalloc模块3.4 起引入是官方提供的 Python 层内存分配追踪调试工具它能记录每个由 Python 分配的内存块的分配栈traceback按文件/行号汇总统计并通过两个快照的差异比对定位内存泄漏。本文基于 CPython 仓库中的 模块文档 与 Python 层实现、C 扩展层、核心 C 实现 展开读完你能够掌握如何用启动参数或运行时 API 开启追踪、如何截取快照并计算 Top 10 / 快照差异、如何格式化内存块的完整 traceback以及各 API 在源码中的真实行为。一、tracemalloc 能提供的三类信息tracemalloc是一个 debug 工具追踪由 Python 分配的内存块即经由 Python 内存分配器的分配它提供以下信息对象分配位置的 traceback分配时调用栈按文件名和行号划分的统计总大小、分配块数量、平均块大小两个快照之间的差异diff用于检测内存泄漏。为了追踪到尽可能多的 Python 内存块应越早启动追踪越好有三种方式方式写法说明环境变量PYTHONTRACEMALLOC1进程启动即追踪可设为帧数如PYTHONTRACEMALLOC25命令行选项python -X tracemalloc或python -X tracemalloc25效果与环境变量等价运行时 APItracemalloc.start(nframe)在代码中任意时刻开启默认情况下每个内存块的 trace 只保存最近 1 帧。若要在启动时保存 25 帧把PYTHONTRACEMALLOC设为25或使用-X tracemalloc25。从源码结构看-X tracemalloc的处理位于解释器生命周期中Python/pylifecycle.c 在初始化阶段读取config-tracemalloc并直接调用_PyTraceMalloc_Start(config-tracemalloc)因此它在解释器对象创建前就已生效能捕获到后续所有模块导入的内存分配——这正是“越早启动越好”的底层原因。二、底层原理挂钩三个内存域分配器start()的文档说明是“install hooks on Python memory allocators安装 Python 内存分配器的钩子”。这一描述可以在核心 C 实现 Python/tracemalloc.c 中得到印证_PyTraceMalloc_Start(int max_nframe)通过PyMem_SetAllocator()依次替换三个内存域的分配器PYMEM_DOMAIN_RAW—— 原始分配对应PyMem_RawMalloc家族PYMEM_DOMAIN_MEM—— 非对象内存PYMEM_DOMAIN_OBJ—— Python 对象内存停止追踪stop()时则把原来保存的分配器函数表恢复回去见 Python/tracemalloc.c 中的恢复逻辑。因此tracemalloc覆盖的是“Python 自己管理的内存”而不是 C 扩展直接malloc出来的系统内存。Python 侧的调用链是tracemalloc.start()Lib/tracemalloc.py 第 9 行from _tracemalloc import *从 C 扩展导入→ C 扩展函数_tracemalloc_start_implModules/_tracemalloc.c→_PyTraceMalloc_Start(nframe)。C 扩展只负责把 10 个底层函数is_tracing、clear_traces、_get_traces、_get_object_traceback、start、stop、get_traceback_limit、get_tracemalloc_memory、get_traced_memory、reset_peak暴露给 Python快照、过滤、统计等逻辑全部用 Python 实现方便阅读与定制。三、实战示例完整继承自官方文档3.1 显示占用内存最多的 Top 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)CPython 测试套件的典型输出官方文档示例[ Top 10 ] frozen importlib._bootstrap:716: size4855 KiB, count39328, average126 B frozen importlib._bootstrap:284: size521 KiB, count3199, average167 B /usr/lib/python3.4/collections/__init__.py:368: size244 KiB, count2315, average108 B /usr/lib/python3.4/unittest/case.py:381: size185 KiB, count779, average243 B /usr/lib/python3.4/unittest/case.py:402: size154 KiB, count378, average416 B /usr/lib/python3.4/abc.py:133: size88.7 KiB, count347, average262 B frozen importlib._bootstrap:1446: size70.4 KiB, count911, average79 B frozen importlib._bootstrap:1454: size52.0 KiB, count25, average2131 B string:5: size49.7 KiB, count148, average344 B /usr/lib/python3.4/sysconfig.py:411: size48.0 KiB, count1, average48.0 KiB可见 Python 从模块中加载了4855 KiB数据字节码与常量collections模块为构建namedtuple类型分配了244 KiB。输出中size... B, count..., average...的格式来自 Lib/tracemalloc.py 中Statistic.__str__它用_format_size把字节数自动换算为 B/KiB/MiB/GiB/TiB 单位小于 100 的数值显示一位小数否则取整average为size / count。3.2 快照差异定位内存泄漏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)测试套件运行前后的差异输出示例[ Top 10 differences ] frozen importlib._bootstrap:716: size8173 KiB (4428 KiB), count71332 (39369), average117 B /usr/lib/python3.4/linecache.py:127: size940 KiB (940 KiB), count8106 (8106), average119 B /usr/lib/python3.4/unittest/case.py:571: size298 KiB (298 KiB), count589 (589), average519 B frozen importlib._bootstrap:284: size1005 KiB (166 KiB), count7423 (1526), average139 B /usr/lib/python3.4/mimetypes.py:217: size112 KiB (112 KiB), count1334 (1334), average86 B /usr/lib/python3.4/http/server.py:848: size96.0 KiB (96.0 KiB), count1 (1), average96.0 KiB /usr/lib/python3.4/inspect.py:1465: size83.5 KiB (83.5 KiB), count109 (109), average784 B /usr/lib/python3.4/unittest/mock.py:491: size77.7 KiB (77.7 KiB), count143 (143), average557 B /usr/lib/python3.4/urllib/parse.py:476: size71.8 KiB (71.8 KiB), count969 (969), average76 B /usr/lib/python3.4/contextlib.py:38: size67.2 KiB (67.2 KiB), count126 (126), average546 B解读模块数据字节码常量总量从3745 KiB涨到8173 KiB比上一快照多4428 KiBlinecache缓存了940 KiB源码用于格式化 traceback全部是两次快照之间新增的。差异计算的实现见 Lib/tracemalloc.py 的_compare_grouped_stats以 traceback 为键分别对新旧快照分组新快照中的键取size_diff stat.size - previous.size旧快照独有即新快照中已释放的键则生成size0, size_diff-previous.size的StatisticDiff。结果按abs(size_diff) → size → abs(count_diff) → count → traceback降序排列StatisticDiff._sort_key。如果系统可用内存紧张快照可以落盘离线分析用Snapshot.dump(filename)写出再用Snapshot.load(filename)读回。从源码看Lib/tracemalloc.py二者就是对Snapshot对象做pickle.dump/pickle.loadpickle.HIGHEST_PROTOCOL。3.3 查看某个内存块的分配 tracebackimport 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)输出示例traceback 限定 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 /usr/lib/python3.4/runpy.py, line 160 __main__, fname, loader, pkg_name)可以看到最多内存分配在importlib模块加载模块数据字节码与常量处共870.1 KiB该 traceback 是importlib最近一次加载数据的位置doctest模块的import pdb行。随着新模块被加载这个 traceback 可能会变化。Traceback.format()的格式化逻辑在 Lib/tracemalloc.py通过linecache.getline取出源码行limit为正取最近limit帧、为负取最旧abs(limit)帧most_recent_firstTrue时反转顺序与traceback.format_tb类似但不含换行符。一个容易踩坑的细节C 层返回的帧序是“最近帧在前”而 Python API 的Traceback约定“最旧帧在前”。Lib/tracemalloc.py 的Traceback.__init__中self._frames tuple(reversed(frames))正是做了这次反转。3.4 Pretty top带源码行的 Top 10 输出import linecache import os import tracemalloc def display_top(snapshot, key_typelineno, limit10): 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)示例输出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 KiBFilter(False, frozen importlib._bootstrap)与Filter(False, unknown)是两条排除过滤规则分别去掉冻结的 importlib 引导代码和空 traceback。filter_traces的过滤语义在 Lib/tracemalloc.py先按inclusive把过滤器分成 include/exclude 两组——若存在 include 过滤器trace 必须至少命中一条才被保留再检查 exclude 过滤器命中任何一条即丢弃。3.5 记录当前值与峰值get_traced_memoryreset_peak下面的代码低效地用“建大列表再求和”的方式计算两次数大列表会临时占用大量内存。用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_size664, first_peak3592984 second_size804, second_peak29704reset_peak的作用是把峰值计数器重置为当前值使得small_sum的峰值能被独立、准确地记录若不调用它second_peak仍会是large_sum计算时的峰值等于first_peak。注意此例中两个峰值都远高于最终驻留内存这提示优化方向——去掉不必要的list()调用直接写sum(range(...))。四、API 参考以下 API 说明完整对应 模块文档。4.1 模块级函数函数说明tracemalloc.start(nframe1)开始追踪在 Python 内存分配器上安装钩子。trace 的 traceback 被限制为nframe帧默认 1仅最近一帧nframe 1。原始总帧数可经Traceback.total_nframe读到。存多帧只对traceback分组或 cumulative 统计有意义帧数越多内存与 CPU 开销越大可用get_tracemalloc_memory()度量。启动期可用PYTHONTRACEMALLOCNFRAME或-X tracemallocNFRAME开启。tracemalloc.stop()停止追踪卸载分配器钩子并清除已收集的全部 trace。想保留现场请先take_snapshot()。tracemalloc.clear_traces()仅清除已分配内存块的 trace不卸载钩子追踪继续。参见stop。tracemalloc.is_tracing()是否正在追踪True/False。tracemalloc.get_traceback_limit()返回 trace 中存储的最大帧数未追踪时抛异常。该限值由start设定。tracemalloc.get_traced_memory()返回元组(current, peak)当前被追踪内存块的总大小与峰值大小字节。tracemalloc.reset_peak()把峰值大小重置为当前大小未追踪时不做事。只改记录的峰值不清除任何 trace因此重置前后的快照仍可有意义地对比。3.9 新增tracemalloc.get_tracemalloc_memory()返回tracemalloc模块自身为存储 trace 占用的内存字节int——评估追踪开销的依据。tracemalloc.get_object_traceback(obj)返回对象obj分配位置的Traceback实例模块未在追踪或该对象未被追踪到时返回None。常与gc.get_referrers、sys.getsizeof配合定位“谁还持有这个对象”。tracemalloc.take_snapshot()返回一个Snapshot实例。快照不包含start之前分配的内存块trace 的 traceback 被截断到get_traceback_limit()帧必须在追踪状态下调用否则RuntimeError见 Lib/tracemalloc.py。4.2 数据类Frame—— traceback 的一帧两个只读属性filenamestr、linenoint。Traceback是Frame序列。Traceback——Frame序列从最旧帧排到最近帧3.7 起改为该顺序。至少 1 帧若取不到帧则用unknown文件、行号 0。做快照时按get_traceback_limit()截断截断前的总帧数存在total_nframe属性中3.9 新增可为None据此可判断 traceback 是否被截断。方法format(limitNone, most_recent_firstFalse)把 traceback 格式化为行列表用linecache取源码行不含换行符。Trace—— 单个内存块的 trace三个只读属性sizeint块大小字节tracebackTraceback分配位置domainint3.6 新增内存块所在的地址空间。tracemalloc 用domain 0追踪 Python 的分配C 扩展可使用其他 domain 追踪别的资源。Snapshot—— 由take_snapshot()创建。属性tracesTrace实例序列顺序未定义统计请用statistics方法traceback_limit做快照时的get_traceback_limit()结果。方法statistics(key_type, cumulativeFalse)—— 按key_type分组返回降序Statistic列表key_type分组维度filename文件名lineno文件名 行号traceback完整 tracebackcumulativeTrue时把 traceback 中所有帧的 size/count 都计入对应分组而不仅最近帧只能与filename、lineno联用校验逻辑见 Lib/tracemalloc.py。排序键为size → count → traceback降序。compare_to(old_snapshot, key_type, cumulativeFalse)—— 与旧快照求差返回降序StatisticDiff列表排序键为abs(size_diff) → size → abs(count_diff) → count → traceback。filter_traces(filters)—— 接受Filter/DomainFilter列表返回过滤后的新Snapshot空列表返回带 trace 副本的新快照。规则所有 inclusive 过滤器一次性生效无任何一条命中即忽略该 trace任一 exclusive 过滤器命中即忽略。dump(filename)/Snapshot.load(filename)—— 快照序列化到文件与读回底层即 pickle。Statistic—— 内存分配统计三个属性count块数、size总字节数、tracebackTraceback实例。由Snapshot.statistics()返回。StatisticDiff—— 新旧快照之间的统计差异由Snapshot.compare_to()返回。属性count新快照中的块数若块已被释放则为0count_diff新旧块数之差若块是新快照新分配的则为0size新快照中的总字节数已释放则为0size_diff新旧总字节数之差新分配的则为0traceback分配位置。4.3 过滤器Filter(inclusive, filename_pattern, linenoNone, all_framesFalse, domainNone)对内存块 trace 做过滤。filename_pattern使用fnmatch.fnmatch通配语法且.pyc扩展名会被替换为.py见 Lib/tracemalloc.py 的_normalize_filename3.5 起不再替换.pyo。官方示例Filter(True, subprocess.__file__)—— 只包含subprocess模块的 traceFilter(False, tracemalloc.__file__)—— 排除tracemalloc模块自身Filter(False, unknown)—— 排除空 traceback。属性inclusiveTrueinclude时只匹配“文件名匹配filename_pattern且行号为lineno”处分配的块Falseexclude时忽略这类块lineno过滤行号None表示匹配任意行filename_pattern文件名模式只读all_framesTrue时检查 traceback 的所有帧False时只查最近一帧。若 traceback 限值为 1此属性无意义domain3.6 新增地址空间int或NoneNone时不按域过滤。tracemalloc 用 domain0追踪 Python 分配C 扩展可用其他 domain。DomainFilter(inclusive, domain)3.6 新增仅按地址空间domain过滤 traceinclusiveTrue时匹配分配到domain地址空间的块False时匹配不在该空间的块。属性inclusive与只读的domain。五、工程实践建议生产环境诊断用启动参数python -X tracemalloc25 app.py或PYTHONTRACEMALLOC25让追踪从解释器初始化开始避免漏掉模块导入阶段的分配帧数按需设置帧越多开销越大用tracemalloc.get_tracemalloc_memory()评估。定位泄漏的标准流程start()→ 取基线快照 → 复现可疑行为 → 取新快照 →compare_to按abs(size_diff)排序查看增长最快的文件行。区分“当前”与“峰值”get_traced_memory()的peak单调不减跨阶段测量时配合reset_peak()分段归零才能看清每个阶段的真实峰值如 3.5 节的例子。快照可离线分析内存紧张时dump到磁盘在另一台机器上load后再做statistics/compare_to避免在线上长时间持有两个快照。注意追踪边界tracemalloc 只挂钩 Python 分配器RAW/MEM/OBJ 三个域见 Python/tracemalloc.cC 扩展直接经系统分配器分配的内存不在追踪范围内快照也不包含start之前的分配。total_nframe判断截断若Traceback.total_nframe大于当前帧数说明 traceback 被nframe限值截断需要更完整的调用链时调大start(nframe)。六、关键源码索引内容路径官方模块文档本文主体依据Doc/library/tracemalloc.rstPython 层实现Filter/Snapshot/Statistic/统计与对比逻辑Lib/tracemalloc.pyC 扩展入口10 个底层函数绑定Modules/_tracemalloc.c核心 C 实现分配器挂钩、trace 存储Python/tracemalloc.c-X tracemalloc启动期接入点Python/pylifecycle.c【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考