我该如何在Python中显式释放内存?

608

我编写了一个Python程序,用于处理一个大输入文件并创建几百万个表示三角形的对象。算法如下:

  1. 读取输入文件
  2. 处理文件并创建一个由顶点表示的三角形列表
  3. 以OFF格式输出顶点列表,后跟三角形列表。三角形由顶点列表中的索引表示。

按照OFF的要求,在输出三角形之前必须打印出完整的顶点列表,这意味着在将输出写入文件之前,必须将三角形列表保存在内存中。但同时由于列表的大小,我遇到了内存错误。

最好的方法是如何告诉Python我不再需要某些数据,它可以被释放?


14
为什么不将三角形打印到一个中间文件中,需要时再将它们读取回来呢? - Alice Purcell
4
这个问题可能涉及到两件相当不同的事情。这些错误是来自同一个Python进程吗?如果是,那么我们需要释放内存到Python进程的堆中,还是来自系统上的不同进程?如果是后者,那么我们需要将内存释放到操作系统中。 - Charles Duffy
你不能简单地使用生成器迭代文件对象,并在完成该组操作后清空列表,以收集活动顶点吗?在无限循环中使用yield file_object.reads,并在数据为空时跳出循环。或者如果是基于行的,只需迭代对象即可?应该是惰性的。 - GRAYgoose124
10个回答

798
根据Python官方文档,你可以通过gc.collect()显式调用垃圾回收器来释放未引用的内存。例如:
import gc

gc.collect()

在使用del标记要丢弃的内容后,您应该执行此操作:

del my_array
del my_object
gc.collect()

33
通常情况下,东西会经常被垃圾回收,除了一些不寻常的情况,因此我认为这不会有太大帮助。 - Lennart Regebro
38
通常情况下应避免使用gc.collect()。垃圾收集器知道如何执行其工作。即便如此,如果提问者处于需要突然释放大量对象的情况(例如数百万个对象),则可能会有用处。 - Jason Baker
244
在循环结束时自行调用 gc.collect() 实际上有助于避免内存碎片化,从而有助于保持性能。我曾看到这样做可以显著改善程序运行时间(据我所记,大约提高了20%)。 - RobM
106
我使用Python 3.6。从 HDF5 加载 Pandas 数据帧(500k 行)后,调用 gc.collect() 将内存使用量从 1.7GB 减少到了 500MB。 - John
70
我需要在一台只有32GB内存的系统中加载和处理多个25GB的numpy数组。在处理完这些数组后,只有使用del my_array然后跟随着gc.collect()才能真正释放内存,确保我的程序能够继续加载下一个数组。 - David
显示剩余18条评论

138

很不幸地(根据你所使用的Python版本和发布版本),某些类型的对象使用“自由列表”,这是一种巧妙的本地优化,但可能会导致内存碎片化,具体表现为将越来越多的内存“指定”为仅用于某种类型的对象,因此无法用于“总基金”。

确保大量但临时使用的内存在完成后返回系统所有资源的唯一可靠方法是使其发生在子进程中,由该子进程执行内存密集型任务然后终止。在这种情况下,操作系统将履行其职责,并欣然回收子进程可能占用的所有资源。幸运的是,在现代版本的Python中,multiprocessing模块使得这种操作(过去非常麻烦)变得不那么糟糕。

在您的用例中,似乎最好的方式是让子进程累积一些结果,同时确保这些结果对主进程可用的方法是使用半临时文件(半临时是指不是在关闭时自动消失的文件,而是您需要在处理完它们后显式地删除它们的普通文件)。


46
我很想看到一个简单的例子来说明这个问题。 - Russia Must Remove Putin
6
认真点,就像 @AaronHall 说的那样。 - Noob Saibot
21
现在可以使用multiprocessing.Manager代替文件实现共享状态,这是一个小例子,详情请查看链接:https://dev59.com/OGAg5IYBdhLWcg3wI4ER#24126616 - user4815162342
1
如果我有一个已打开的文件指针列表,我是需要 1) 删除整个列表还是 2) 逐个删除列表中的每个元素,然后调用 gc.collect() 呢? - Charlie Parker
@CharlieParker 假设列表是 x = [obj1, obj2, ...obj20]。为了释放内存,可以采取以下任何一种措施:(1) del x (2) x=[] (3) del x[:]。只是对于方法(1),变量 x 被删除且不再可访问,因此列表 x 的内存也将被释放。而对于方法 (2) 和 (3),x 仍然可访问并且仍然占用内存。 - AnnieFromTaiwan
@CharlieParker 最后但并非最不重要的是,gc.collect() 不是必需的,因为列表中的内容是对象,只要它们的引用计数为0,对象的内存就会立即释放。只有当列表的内容是整数或浮点数时,才需要执行 gc.collect() - AnnieFromTaiwan

73
del语句可能会有用,但是据我所知它不能保证释放内存。可以查看此处的文档 ... 以及此处的原因为什么不被释放。

我听说过Linux和Unix类型系统上派生一个Python进程来完成一些工作,获取结果然后终止它。

本文有关于Python垃圾回收器的注释,但我认为缺乏内存控制是托管内存的缺点


IronPython和Jython是否是避免这个问题的另一个选择? - Esteban Küber
@voyager:不,任何其他语言都不会。问题在于他将大量数据读入列表中,而该数据对于内存来说太大了。 - Lennart Regebro
1
在IronPython或Jython下,情况可能会更糟。在这些环境中,如果没有任何其他引用,甚至不能保证内存会被释放。 - Jason Baker
@voyager,是的,因为Java虚拟机全局寻找要释放的内存。对于JVM来说,Jython没有什么特别之处。另一方面,JVM也有自己的缺点,例如您必须预先声明它可以使用多大的堆。 - Prof. Falken
这是Python垃圾回收器的相当糟糕的实现。Visual Basic 6和VBA也有托管内存,但从来没有人抱怨过那里的内存没有被释放。 - Anatoly Alekseev
如果我有一个已打开的文件指针列表,我是需要 1) 删除整个列表还是 2) 逐个删除列表中的每个元素,然后调用 gc.collect() 呢? - Charlie Parker

43

Python自带垃圾回收功能,所以如果你减小列表的大小,它将重新回收内存。你也可以使用"del"语句完全删除一个变量:

biglist = [blah,blah,blah]
#...
del biglist

34
这是有道理的,但也不完全正确。虽然减小列表的大小可以释放内存,但并不能保证内存何时被释放。 - user142350
4
不,但通常会有所帮助。然而,我理解这里的问题是,如果将所有对象读入列表中,在处理完它们之前就会因内存不足而无法继续。在处理完成之前删除列表不太可能是一个有用的解决方案。 ;) - Lennart Regebro
4
请注意,“del”并不保证对象一定会被删除。如果还有其他引用指向该对象,它将不会被释放。 - Jason Baker
5
大列表 biglist = [ ] 会释放内存吗? - neouyghur
5
是的,如果旧清单没有被其他任何东西引用。 - Ned Batchelder
显示剩余4条评论

31

(del可以帮助你标记对象为可删除状态,当没有其他参考引用它们时。通常情况下,CPython解释器会保留这段内存以备后用,所以你的操作系统可能看不到这些“释放”的内存。)

如果使用更紧凑的数据结构,也许您在首次就不会遇到任何内存问题。 因此,数字列表比标准array模块或第三方numpy模块使用的格式要少得多。将顶点放在NumPy 3xN数组中,并将三角形放在N元素数组中,可以节省内存。


嗯?CPython的垃圾回收是基于引用计数的;它不是周期性的标记和扫描(对于许多常见的JVM实现),而是在其引用计数达到零时立即删除某些东西。只有循环(其中引用树中的循环导致引用计数为零但实际上不为零)需要定期维护。del并没有做任何重新分配对象所有名称引用的不同值的事情。 - Charles Duffy
我理解你的意思:我会相应地更新答案。我知道CPython解释器实际上以某种中间方式工作:del从Python的角度释放内存,但通常不从C运行时库或操作系统的角度释放。参考资料:https://dev59.com/v1wY5IYBdhLWcg3w0KrL#32167625,http://effbot.org/pyfaq/why-doesnt-python-release-the-memory-when-i-delete-a-large-object.htm。 - Eric O. Lebigot
就您提供的链接内容而言,我们是同意的。但是假设OP所说的错误是来自同一个Python进程,那么在释放内存到进程本地堆和操作系统之间的区别似乎不太相关(因为释放到堆中会使该空间在该Python进程内可用于新的分配)。对此,del与退出作用域、重新赋值等同样有效。 - Charles Duffy

28

你不能显式地释放内存。你需要做的是确保你不保留对象的引用。那么它们将被垃圾收集,从而释放内存。

在你的情况下,当你需要大量列表时,通常需要重新组织代码,通常使用生成器/迭代器代替。这样你就根本不需要在内存中拥有大型列表。


3
如果这种方法可行,那么很可能值得一试。但需要注意的是,在迭代器上无法进行随机访问,这可能会引起问题。 - Jason Baker
2
没错,如果需要的话,那么随机访问大型数据集很可能需要某种类型的数据库。 - Lennart Regebro
你可以轻松地使用迭代器从另一个迭代器中提取随机子集。 - S.Lott
真的,但是你必须遍历所有内容才能获取子集,这将非常慢。 - Lennart Regebro

16

我曾经遇到过类似的问题,需要从文件中读取图形。处理过程包括计算一个200 000x200 000的浮点矩阵(逐行处理),但该矩阵无法放入内存。尝试在计算之间使用gc.collect()释放内存解决了与内存相关的问题,但导致性能问题:我不知道为什么,即使使用的内存量保持恒定,每次新调用gc.collect()所需时间都比上一次多一些。因此,很快垃圾收集占据了大部分计算时间。

为了解决内存和性能问题,我采用了一种多线程技巧,这是我在某个地方读到的(很抱歉,我找不到相关的帖子了)。在此之前,我正在一个大型for循环中逐行读取文件,处理它,并偶尔运行gc.collect()来释放内存空间。现在我调用一个函数,在新线程中读取和处理文件的一块内容。一旦线程结束,内存就会自动释放,而不会出现奇怪的性能问题。

实际上,它的工作原理如下:

from dask import delayed  # this module wraps the multithreading
def f(storage, index, chunk_size):  # the processing function
    # read the chunk of size chunk_size starting at index in the file
    # process it using data in storage if needed
    # append data needed for further computations  to storage 
    return storage

partial_result = delayed([])  # put into the delayed() the constructor for your data structure
# I personally use "delayed(nx.Graph())" since I am creating a networkx Graph
chunk_size = 100  # ideally you want this as big as possible while still enabling the computations to fit in memory
for index in range(0, len(file), chunk_size):
    # we indicates to dask that we will want to apply f to the parameters partial_result, index, chunk_size
    partial_result = delayed(f)(partial_result, index, chunk_size)

    # no computations are done yet !
    # dask will spawn a thread to run f(partial_result, index, chunk_size) once we call partial_result.compute()
    # passing the previous "partial_result" variable in the parameters assures a chunk will only be processed after the previous one is done
    # it also allows you to use the results of the processing of the previous chunks in the file if needed

# this launches all the computations
result = partial_result.compute()

# one thread is spawned for each "delayed" one at a time to compute its result
# dask then closes the tread, which solves the memory freeing issue
# the strange performance issue with gc.collect() is also avoided

2
我想知道为什么你在Python中用//而不是#来进行注释。 - user4396006
2
我把语言搞混了。谢谢您的提醒,我已经更新了语法。 - Retzod

12

正如其他回答所说,即使Python代码不再使用内存(因此gc.collect()没有释放任何内容),在长时间运行的程序中,Python也可以避免将内存释放到操作系统中。 但是,如果您在Linux上,可以尝试通过直接调用libc函数malloc_trim来释放内存(man page)。 类似于:

import ctypes
libc = ctypes.CDLL("libc.so.6")
libc.malloc_trim(0)

我该如何将要删除的对象的引用传递给您建议的库?我有变量名称,我应该这样做 lib.malloc_trim(var) 吗? - Charlie Parker
很抱歉,malloc_trim不是这样工作的(请参阅手册页)。此外,我认为libc不知道Python变量名的任何信息,因此这种方法不适合处理变量。 - Joril
@Yahya 哎呀!我不知道会有这种副作用... 你能分享一下它们是什么属性吗?也许是弱引用吗? - undefined
这只是一个普通的类属性,但在删除它之后,错误仍然被抛出。所以,我不确定它是否是原因。让我删除我的旧评论并给这个点赞。 - undefined

11

其他人已经发布了一些方法,你可能能够“劝说”Python解释器释放内存(或避免出现内存问题)。你应该先尝试他们的想法。然而,我认为直接回答你的问题很重要。

实际上并没有直接告诉Python释放内存的方法。事实是,如果你想拥有那么低的控制级别,你需要用C或C++编写扩展程序。

话虽如此,这里有一些工具可以帮助处理这个问题:


4
当我使用大量内存时,gc.collect()和del gc.garbage[:]可以很好地工作。 - Andrew Scott Evans

4
如果您不在意顶点的重复使用,您可以有两个输出文件——一个用于顶点,一个用于三角形。然后在完成时将三角形文件附加到顶点文件中。

2
我想我可以只在内存中保留顶点,并将三角形打印到文件中,然后仅在最后打印顶点。但是,将三角形写入文件的操作会严重影响性能。有没有办法加速这个过程? - Nathan Fellman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接