服务端阅读 06月2日 01:36
Python 内存管理是怎样的?引用计数、分代 GC 和内存池原理
Python 内存管理分三层:引用计数(主要)、垃圾回收(处理循环引用)、内存池(减少 malloc 开销)。日常开发不需要手动管理内存,但理解机制能帮你排查内存泄漏。引用计数:核心机制每个对象维护一个引用计数 ob_refcnt。引用增加时 +1,引用减少时 -1,归零时立即释放。import sysa = [1, 2, 3] # 引用计数 1b = a # 引用计数 2c = a # 引用计数 3print(sys.getrefcount(a)) # 4(多 1 是因为 getrefcount 参数本身也是引用)del b # 引用计数 2c = None # 引用计数 1# a 离开作用域后引用计数归零,内存释放引用计数的优势:实时释放,不需要暂停程序做垃圾回收。劣势:无法处理循环引用。循环引用问题a = []b = []a.append(b) # a 引用 bb.append(a) # b 引用 adel a, b # 引用计数各剩 1(互相引用),永远不会归零引用计数对循环引用无能为力。Python 用分代垃圾回收(GC)处理这种情况。分代垃圾回收GC 把对象分成三代:第 0 代(新对象)、第 1 代、第 2 代(长寿对象)。新创建的对象在第 0 代经过一次 GC 存活的对象晋升到下一代第 0 代 GC 最频繁(阈值约 700 个对象),第 2 代最少分代回收的理论依据:大部分对象很快变成垃圾(如函数内的临时变量),长寿对象倾向于一直活着。只频繁检查年轻对象,减少 GC 开销。import gcprint(gc.get_threshold()) # (700, 10, 10) — 第0代阈值700,每10次第0代GC触发1次第1代print(gc.get_count()) # 当前各代对象计数手动触发 GC:gc.collect()。通常不需要手动调用,但在处理大量循环引用对象后可以主动回收。内存池:pymallocPython 不直接用系统的 malloc/free 管理小对象,而是用自己实现的 pymalloc 内存池:小对象(:由 pymalloc 从大块内存中分配,减少系统调用大对象(>=512 字节):直接用系统 malloc内存池按 256 KB 的 arena 分块,arena 内按 4 KB 的 pool 分块,pool 内按固定大小的 block 分配。相同大小的 block 共享 pool,减少碎片。这就是为什么 Python 进程的 RSS(常驻内存)不会随着对象释放而下降——pymalloc 保留空闲 arena 供后续使用,不归还给操作系统。内存泄漏排查Python 的内存泄漏通常是"对象被意外引用导致无法释放"而非真正的泄漏。1. 用 objgraph 找到引用链import objgraphobjgraph.show_backrefs(objgraph.by_type("dict")[0], max_depth=5)生成引用关系图,找到谁在持有不该持有的引用。2. 用 tracemalloc 定位分配位置import tracemalloctracemalloc.start()# ... 运行代码 ...snapshot = tracemalloc.take_snapshot()top_stats = snapshot.statistics("lineno")for stat in top_stats[:10]: print(stat)显示内存分配最多的代码行。3. 常见泄漏原因全局列表/字典不断追加但从不清理闭包或回调持有大对象引用__del__ 方法导致对象无法被 GC 回收(Python 3.4 已改善)缓存没有上限(用 functools.lru_cache 替代手动缓存)追问为什么 Python 进程的内存只增不减?pymalloc 保留空闲 arena 不归还操作系统。如果你看到 RSS 很高但对象不多,可能是内存碎片。malloc_trim(0) 可以强制归还(Linux),但不保证生效。引用计数和 GC 哪个先执行?引用计数是实时的,每次赋值/删除都更新。GC 是周期性运行的,只处理循环引用。两者配合工作。