用__slots__+array.array或struct可减少70%+内存并缓解碎片,因默认对象含__dict__等开销240字节,而pymalloc对小对象(≤512B)按块管理,混尺寸释放后Block无法合并,导致Arena滞留、RSS上涨。

直接用 __slots__ + array.array 或 struct 替代普通类和列表,能砍掉 70%+ 内存并显著缓解碎片——因为默认对象布局带 __dict__ 和哈希表头,每实例固定吃掉 ~240 字节,而碎片主要来自 pymalloc 对小块内存的离散管理。
为什么小对象会加剧内存碎片?
CPython 的 pymalloc 把小对象(≤512B)按固定大小切分成 Block,塞进 Pool(4KB)再装进 Arena(256KB)。一旦你混着创建不同尺寸的小对象(比如 dict、str、自定义类实例),释放后空闲 Block 就散落在 Pool 里,无法合并;整个 Pool 又得等所有 Block 都空了才能归还 OS。结果就是:RSS 持续上涨,但 tracemalloc 看不到明显对象增长。
- 高频短命对象(如解析 JSON 的
dict、循环生成的namedtuple)会快速填满第 0 代 GC,触发扫描,间接加重 pymalloc 压力 -
list.append()动态扩容时会申请新内存块,旧块若未被复用就成碎片源头 - 字符串拼接用
+=会产生大量中间str对象,每个都是独立小对象
怎么用 __slots__ 真正压住内存?
__slots__ 不是加了就生效,它靠编译期锁定属性名,把实例从动态字典变成紧凑 C 结构体。但错一步就白忙:
- 必须声明为字符串元组:
__slots__ = ('x', 'y'),写成列表或单个字符串会静默失效 - 继承链上每一级都要单独声明,父类写了子类没写 → 子类实例自动重建
__dict__ - 字段名必须和赋值完全一致,
self.x_却在__slots__里写'x'→ 运行时报AttributeError - 别和
@dataclass(slots=True)混用,后者已内置优化,手动加__slots__反而出错
验证效果别只看理论:用 sys.getsizeof() 对比同数据量的普通类和 __slots__ 类,注意属性值类型要一致(比如都用短字符串)。
立即学习“Python免费学习笔记(深入)”;
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
array.array 和 struct 怎么替代 list/dict?
当你要存百万级同类型数值(如坐标、ID、状态码),list 是最差选择——每个元素都是指针,额外开销大且内存不连续。改用:
-
array.array('i', [1, 2, 3]):存 int,每个元素占 4 字节,无指针、无对象头,比list省 3 倍内存 -
struct.pack('ii', x, y)批量序列化结构体,再用memoryview随机访问,彻底绕过对象分配 - 对固定字段结构,用
namedtuple(不可变)比普通类轻,但不如__slots__极致;dataclass(frozen=True)也类似
注意:array.array 只支持数值类型,字符串得用 bytearray 或 memoryview + 编码预处理。
哪些“省事”操作反而在制造碎片?
看着方便的写法,往往是碎片温床:
-
my_list = []循环内反复新建 → 丢弃旧引用,触发 pymalloc 新分配;换成my_list.clear()复用容器 -
s += 'a'拼接字符串 → 每次都生成新str对象;改用''.join(chunks)或io.StringIO -
gc.disable()后忘了gc.enable()或没配gc.collect(0)→ 内存滞留,Arena 无法回收 - 用
weakref.ref(obj)解循环引用 → 弱引用本身也是对象,堆在第 0 代里,高频创建反而加剧扫描压力
真正关键的是监控:用 psutil.Process().memory_info().rss 看 RSS 是否持续爬升,再结合 gc.get_count() 观察第 0 代增长速率——调阈值不是目的,确认碎片是否真被缓解才是。

















