海量小对象使GC变慢,因Python默认第0代阈值仅700次分配,高频创建短命对象导致频繁扫描;调高阈值至5000并配合gc.disable()/collect(0)可降GC耗时90%;__slots__和array.array能大幅减少内存开销;需规避隐式循环引用并监控gc统计。

为什么海量小对象会让GC变慢
Python默认把新对象都放进第0代,而第0代的回收阈值是700次对象分配。当程序高频创建短命小对象(比如解析JSON时的dict、list,或循环中生成的namedtuple),第0代很快被填满,触发频繁的gc.collect(0)——每次都要扫描所有第0代对象,哪怕其中99%刚出生就死了。这不是CPU瓶颈,而是扫描开销本身在拖慢吞吐。
调高第0代阈值并手动控制回收时机
盲目禁用GC风险大,但把第0代阈值从默认(700, 10, 10)拉高到(5000, 10, 10)能显著减少扫描频次。关键是要配合业务节奏手动触发:
- 在批量处理前调用
gc.disable(),避免中间穿插GC打断流程 - 批量结束后立刻调用
gc.collect(0),只清理最可能堆积的第0代 - 避免用
gc.collect()全代扫描——它会强制检查第2代,代价远高于第0代
示例:gc.set_threshold(5000, 10, 10)之后,一个处理10万条日志的循环,GC耗时可从800ms降到90ms。
用__slots__和array.array替代动态对象
每个普通Python对象都有__dict__和额外内存开销(约56字节)。当创建百万级Point(x, y)实例时,光元数据就吃掉50MB+。这时:
立即学习“Python免费学习笔记(深入)”;
- 给类加
__slots__ = ('x', 'y'),去掉__dict__,单个实例内存降为16字节 - 若字段类型统一(如全是
float),直接用array.array('d', [x1,y1,x2,y2,...])存原始数据,比对象列表省3倍内存 -
dataclass(frozen=True)比普通类稍好,但不如__slots__彻底
警惕隐式循环引用:弱引用不是万能解药
用weakref.ref破循环确实有效,但容易忽略两点:
- 弱引用本身有开销,每多一个
weakref就多一个对象,反而加剧第0代压力 -
weakref.ref(obj)()返回None时需额外判空,业务逻辑变复杂 - 真正该做的是设计上规避——比如树节点不存
parent,改用栈路径回溯;或用ID映射代替对象直连
最常被忽略的点:gc.collect()返回值是回收的对象数量,但它不告诉你哪些代被清了。生产环境必须用gc.get_stats()(Python 3.12+)或定期gc.get_count()监控第0代增长速率,否则调阈值只是掩耳盗铃。


















