bigywater.tech
Python · 26

Python 内存分配器:为什么释放对象后 RSS 不一定下降

分配再释放一批bytearray,用resource.getrusage测RSS前后变化,看Python对象确实被销毁之后,进程占用的物理内存为什么没有相应下降。

+275MB
分配20万个bytearray带来的RSS涨幅
0下降
del+gc.collect()后RSS没有降低
pymalloc arena
内存保留在Python分配器内部,没还给OS

做的事:分配一大块内存,再全部释放

很多人以为del之后加个gc.collect(),进程占用的内存就该降回去。

代码import resource, gc def rss_kb(): return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss before = rss_kb() big = [bytearray(1024) for _ in range(200000)] # 约200MB数据 after_alloc = rss_kb() del big gc.collect() after_del = rss_kb()

验证:分配后RSS暴涨了约275MB

这一步符合预期,分配内存确实要占用的物理页。

结果RSS before(bytes): 19791872 RSS after alloc(bytes): 308314112 delta: +288522240 bytes(约275MB)

这里的ru_maxrss在macOS上单位是字节而不是KB(不同平台单位不同,这是一个容易踩的坑,Linux上ru_maxrss的单位才是KB),换算下来,分配前进程占用约18.9MB的基线,分配后多涨了约275MB,跟200000×1024字节≈195MB的bytearray理论大小加上Python对象本身的元数据开销是同一量级,这一步是符合直觉的。

验证:del+gc.collect()之后,RSS一点没降,反而多涨了16KB

这才是真正违反直觉的地方。

结果RSS after del+gc.collect()(bytes): 308330496 delta(after_del - before): +288538624 (比分配后的峰值308314112还多了16384字节,没有任何下降)
的原因分两层:第一层,20万个bytearray对象本身没有循环引用,del big 这一句就让它们的引用计数归零,对象立刻被销毁,占用的内存被归还给了CPython自己的内存分配器(pymalloc这一层)——gc.collect()在这里其实是多余的,因为没有循环引用需要垃圾回收器介入;第二层才是关键:CPython的pymalloc管理着一批从操作系统申请来的"内存池"(arena),对象释放后,这块内存变成"pymalloc内部空闲",但pymalloc出于的性能考虑,只有当一整个arena(通常是1MB)里的内存块全部变空时才会考虑把这个arena还给操作系统——如果这20万个bytearray分散在很多arena里,每个arena里都还残留着别的存活对象,pymalloc就不会归还任何一块给OS,进程的RSS(操作系统统计口径)自然观察不到下降。这不是bug,是CPython内存管理的设计权衡:频繁向操作系统申请和归还内存本身有的系统调用开销,pymalloc选择了"倾向于保留、少归还"。

分步演示:释放的内存去哪了

每一步都是本文上面用resource.getrusage跑出来的原始结果。

对象被释放了,但RSS没有下降
这一步新验证的阶段

    本文用到的工具

    • 本机 Python 3.14.7
    • resource.getrusage / gc.collect():测量分配与释放前后的进程RSS
    京ICP备2026067502号-1京公网安备11010502063487号