← 中文资源

简体中文版本

Python — 对象已释放内存仍增长:gc 引用循环

排查手动 del 之后仍增长的 Python 内存,用 gc.get_referrers 与 objgraph 找引用循环,并用 weakref 或显式 close 打破。

阅读英文原文

症状

  悖论本身就是线索:每个对象都 del 了,tracemalloc 的 current 回到基线,RSS 却逐请求爬升。此时该给“对象群体”而不是“字节”装仪表:objgraph.show_growth 显示某类型计数随请求数线性增长——那就是泄漏群体。再任取一个成员审问它的保活者:gc.get_referrers 返回持有它的注册表、栈帧或闭包,这些就是需要切断的环边。另一个信号是 gen2 回收在发生而内存不归还,以及 gc.garbage 非空——含 del 的循环会被永久停进不可回收列表。

诊断思路

  先按类型统计对象增长(objgraph 前后对比),锁定泄漏群体;再用 get_referrers 找出保活链——长生命周期注册表与每请求对象互相引用、缓存键值互指加自指、异常对象通过 traceback 持有局部帧并与处理器闭包成环,是三大常见边;最后 gc.collect() 后看 gc.garbage 长度,非空即存在收集器拒绝触碰的 del 循环。

根因

  引用计数只在无引用时立即释放对象。循环让每个成员计数恒大于零,回收只能寄希望于分代 GC 在恰当时机运行;含 del 的循环可能被永久停进 gc.garbage。长生命周期容器把每请求对象卷进循环,就把瞬态数据变成了永久驻留——del 无济于事,因为计数清不到零。

修复与验证

  在设计层破环:反向指针(父链接、实例缓存)用 weakref.ref 或 WeakValueDictionary;容器清空用显式 close 或上下文管理器在退出时 del 成员;资源所有者放弃 del 改用上下文管理器协议(直接删除 del 会失去清理保证,必须替换而非移除);存储异常对象时先 exc.traceback = None 截断帧链。验证时重复跑批次,断言各类型对象计数回到基线、RSS 进入平台期、完全收集后 gc.garbage 保持为空——三个断言合起来证明环已断且不可回收列表不再积累。

代码示例

import objgraph
objgraph.show_growth(limit=10)
# func_with_closure   1250  +1250
# RequestContext      1250  +1250
import gc
sample = [o for o in gc.get_objects() if isinstance(o, RequestContext)][0]
print(gc.get_referrers(sample)[:3])
# [dict of registry, <frame ...>, the handler closure]
import weakref

class Node:
    def __init__(self, parent):
        self.parent = weakref.ref(parent)  # does not keep parent alive
        self.children = []
from weakref import WeakValueDictionary

instances = WeakValueDictionary()  # id -> instance, no retention
class Session:
    def close(self):
        for child in self.children:
            child.parent = None   # break cycles deterministically
        self.children.clear()

    def __enter__(self): return self
    def __exit__(self, *exc): self.close()