内存分配与观测
内存分配与观测
对象已经不再存活,并不等于进程 RSS 必须下降。把对象生命周期、Python 分配器和操作系统的驻留内存分层看,才能避免把缓存复用都误判成泄漏。
从对象到分配器
内存堆与算法堆
不是。内存堆是动态分配对象的空间;heapq 的堆是优先队列数据结构。普通 CPython 对象由运行时在私有堆上管理,不是说每次创建对象都调用一次系统 malloc。
小对象分配
减少频繁申请/释放小块内存的系统分配开销。CPython 3.12 默认 pymalloc 优化不超过 512 字节的分配请求;这个数字指请求大小,不是说「字符串字符数不超过 512」就必然走某条路径。
arena、pool 与 block
可把它们理解为较大内存区、其中按大小类管理的池、实际分配块。相同大小类复用空闲块,减少零碎系统分配;它们是分配层次,不是 GC 的年轻代、老年代。
对象本身的开销
Python 对象除了值还有类型、管理信息及对齐等开销;列表还保存元素引用和预留容量。不能按「一百万个数字×8 字节」直接估普通 list[int],需要考虑共享对象与实际结构。
浅层大小与共享引用
通常不会,它返回对象自身报告的大小,容器指向的对象不会全算进去。列表自身不大,元素却可能持有巨大内容;共享元素也不能在深度统计时重复累加。精确值还依平台与版本而变。
观测指标
弱引用容器与 slots
弱值表不保活值,不适合承诺结果一直存在;对象必须支持弱引用。常见纯 slots 类要加入 weakref 才能弱引用;继承链可能已经提供它,不能在所有子类里重复声明后还认为布局一定相同。
递归统计的难点
需要去重共享对象并防环,还需决定是否算类、模块、外部缓冲与分配器开销。递归 getsizeof 得到的只是所选边界下估算;同一对象在多个容器里出现,不代表实际内存被复制了多份。
对象销毁与 RSS
空闲块可能留在 freelist、pymalloc arena 或系统分配器中,碎片使区域仍有活块,也可能只是高水位缓冲。RSS 是驻留页指标,不等于活对象大小;应同时看活分配、引用链、RSS 和后续是否复用。
分配和释放的接口配对
不能。C 扩展必须按分配域与 API 配对,如 PyMem_Malloc/PyMem_Free,不能用 free 释放 Python 管理器分配的对象。错误配对可能内存破坏,不是简单少执行一次 GC;不同构建还可能换分配器。
RSS、USS 与 PSS
RSS 包含该进程驻留的共享页,多进程直接相加可能重复;USS 关注独占页,PSS 按共享比例摊分。容器是否 OOM 还取决于 cgroup 记账与限制;指标需注明系统与采集工具,不把一种数字当全部内存真相。
tracemalloc 的覆盖范围
它追踪接入 Python 内存追踪机制的分配,不能默认覆盖 C 库直接 malloc、GPU 内存或全部 mmap。尽早启动并比较相同负载前后快照;若 RSS 涨而跟踪量稳定,继续调查原生缓冲和其他驻留来源。
流式处理与内存视图
生成器按需产出,array 等紧凑结构减少逐对象成本,memoryview 可避免一些拷贝。但惰性生成器仍可能握着大输入,view 也可能保活整个底层 buffer;省拷贝和省存活内存要分别测。
实验:比较 Python 分配快照
# 可独立运行
import tracemalloc
tracemalloc.start()
try:
before = tracemalloc.take_snapshot()
payloads = [bytearray(1024) for _ in range(128)]
after = tracemalloc.take_snapshot()
changes = after.compare_to(before, "lineno")
assert len(payloads) == 128
assert any(change.size_diff > 0 for change in changes)
del payloads
finally:
tracemalloc.stop()这里只验证能观察到分配变化,不要求某个固定字节数,也不据此断言 RSS 一定下降。
按证据排查
RSS 上升而 Python 跟踪量稳定
先确认追踪启动时机和负载是否可比,再看原生扩展、文件映射、线程栈、缓冲、共享页与分配器碎片。用 OS 指标和能覆盖原生分配的工具补证据;只多调几次 gc.collect 无法解释未被追踪的全部空间。
证明持续保留的引用
固定负载重复运行,比较清理后的活分配与对象数量,找到持续增长的分配位置,再追持有链,例如全局 dict→回调→闭包→大对象。tracemalloc 说明哪里分配,不直接证明谁保活;诊断列表和快照也有开销。
GC 暂停与尾延迟
低开销记录 gc.callbacks 的 start/stop、代际与耗时,关联请求长尾,并区分引用计数销毁、循环收集和其他 CPU 工作。先减少环与短命大图,再评估阈值;实验固定版本和负载,不先关 GC 宣称问题已解决。
fork 与写时复制
部分普通对象访问会产生引用计数写入,GC 也可能改管理元数据,触发写时复制。gc.freeze 可减少冻结对象被循环收集扫描时的写入,不能消除所有引用计数/业务写;用 PSS/USS 和私有脏页评估。
并发下的内存预算
列基础常驻、每个在途任务峰值、解析子进程、模型/文件缓冲、缓存和发布重叠,再留余量。并发数乘任务平均内存会低估尖峰;项目用 gevent,不能把 prefork 的每子进程自动回收配置当通用兜底。
生成器与结果物化
可能上游驱动已全部缓冲、下游 list/排序/拼接重新聚合,或每条对象仍被缓存/集合持有。逐段检查读取、转换、输出和关闭责任;分批读取加按需写出,且处理慢接收方的背压。
slots 的实际收益
实例数少、字段本身很大、父类已有 dict、子类重新引入字典,都会削弱收益。看同样模型的整体分配而不是只比较 sys.getsizeof(instance),还要验证序列化、继承和弱引用需求。
手动 GC 的使用范围
通常不能。它无法回收仍有强引用的缓存,也不保证原生分配器归还驻留页,还会增加延迟;释放大对象本身也可能耗时。先查峰值、泄漏、缓存和并发,再只在可测收益的受控场景考虑主动收集。
按层解释内存未归还
先问指名字、对象、分配块还是 RSS。再检查强引用/环→引用计数与循环 GC→分配器复用与碎片→原生缓冲和 OS 驻留,最后用快照、持有链和进程指标验证;每层都有不同证据,不能一句「Python 有 GC」就结束。
底层扩展的引用环支持见循环 GC 协议;分配器接口见Python 内存管理。下一篇:源码、字节码与执行。
