如何在内存密集型过程之间清除 Python 中的内存

112 阅读3分钟

在 Python 中,当需要顺序读取多个大型文本文件、在内存中存储大量数据,然后使用这些数据写入一个大型文件时,遇到了内存管理的问题。这些读/写周期是单独完成的,并且没有公共数据,因此不需要在它们之间共享任何内存。

尝试将这些过程放在一个脚本中,希望在 RAM 填满时垃圾回收器会删除旧的、不再需要的对象。然而,事实并非如此。即使在周期之间显式删除了对象,也需要比单独运行过程长得多的时间。

具体来说,进程会挂起,使用所有可用的 RAM 但几乎没有 CPU。调用 gc.collect() 时它也会挂起。因此,决定将每个读/写过程拆分为单独的脚本,并使用 execfile() 从中心脚本调用它们。遗憾的是,这并没有解决任何问题;内存仍然堆积。

使用了简单、显而易见的方法,即从 shell 脚本调用子脚本,而不是使用 execfile()。然而,想知道是否有办法让这种方法起作用。

2、解决方案 任何没有引用的 CPython 对象都会立即释放。Python 会定期执行垃圾回收,以处理仅相互引用但程序不可达的对象组(循环引用)。如果需要在特定时间清除这些垃圾,可以手动调用垃圾回收器(gc.collect())。这会让内存可供 Python 脚本重新使用,但可能不会立即(或永远)将该内存释放回操作系统。

CPython 在 256KB 的区域中分配内存,将其划分为 4KB 的池,再进一步细分为块,这些块专用于特定大小的对象(这些块通常是类似类型,但不一定是)。此内存可以在 Python 进程中重复使用,但在整个区域为空之前不会将其释放回操作系统。

在 2005 年之前,一些常用的对象类型不使用此方案。例如,一旦创建了 'int' 或 'float',即使该内存已被 Python 释放,也不会将其返回给操作系统,但可以将其重新用于这些类型的其他对象。(当然,小的 int 是共享的,不会占用任何额外的内存,但是如果你分配了,比如说,一个大的 int 或 float 的列表,即使这些对象被释放,CPython 也会保留该内存。)Python 还会保留由列表和字典分配的一些内存(例如,最近的 80 个列表)。

所有这些都根据这份关于 Python 2.3 左右内存分配器改进的文件。我了解从那时起已经进行了一些进一步的工作,因此一些细节可能已经改变(根据 arbautjc 在下面的评论,int/float 情况已经得到纠正),但基本情况仍然是:出于性能原因,Python 不会立即将所有内存返回给操作系统,因为 malloc() 对于小分配来说开销相对较大,并且随着内存碎片化程度的增加而变得越来越慢。因此,Python 只会 malloc() 较大块的内存,并在这些块内部分配内存,并且仅在这些块完全为空时才将它们返回给操作系统。

您可以尝试使用其他 Python 实现,例如 PyPy(旨在尽可能与 CPython 兼容)、Jython(在 JVM 上运行)或 IronPython(在 .NET CLR 上运行),以了解它们的内存管理与您所做的事情是否更匹配。如果您当前正在使用 32 位 Python,则可以尝试使用 64 位 Python(假设您的 CPU 和操作系统支持)。

然而,我个人认为从 shell 脚本顺序调用脚本的方法完全没问题。可以使用 subprocess 模块用 Python 编写主脚本,但使用 shell 可能更简单。

不过,在不了解脚本的具体功能的情况下,很难猜测是什么导致了这种情况。