在Python中释放内存
我有几个关于内存使用的小问题,下面是一个例子。
如果我在解释器中运行,
foo = ['bar' for _ in xrange(10000000)]
我机器上实际使用的内存会增加到
80.9mb
。然后,我再del foo
实际内存会下降,但只降到
30.4mb
。解释器的基础内存使用是4.4mb
,那么不把26mb
的内存释放给操作系统有什么好处呢?是因为Python在“提前规划”,想着你可能会再次使用那么多内存吗?为什么具体释放了
50.5mb
- 这个释放的数量是根据什么决定的呢?有没有办法强制Python释放所有使用过的内存(如果你知道之后不会再用那么多内存)?
注意
这个问题和 如何在Python中显式释放内存? 不同,因为这个问题主要讨论的是即使在解释器通过垃圾回收释放了对象后,内存使用量仍然从基础值增加的情况(无论是否使用 gc.collect
)。
4 个回答
eryksun 回答了第一个问题,我回答了第三个问题(原来的第四个问题),现在我们来回答第二个问题:
为什么它特别释放了 50.5MB 的内存 - 这个释放的量是基于什么的?
这个释放的量,最终是基于 Python 和 malloc
内部一系列很难预测的巧合。
首先,根据你测量内存的方式,你可能只是在测量实际映射到内存中的页面。在这种情况下,每当一个页面被交换出去,内存就会显示为“已释放”,尽管实际上它并没有被释放。
或者你可能是在测量正在使用的页面,这可能会计算那些分配了但从未使用的页面(在像 Linux 这样的系统中,它们通常会过度分配),或者那些被标记为 MADV_FREE
的页面等等。
如果你确实是在测量已分配的页面(这其实不是一个很有用的做法,但看起来你就是在问这个),而且页面确实被释放了,这种情况可能发生在两种情况下:要么你使用了 brk
或类似的方式缩小数据段(这种情况现在很少见),要么你使用了 munmap
或类似的方式释放了一个映射的段。(理论上还有一种小变体,就是有方法可以释放映射段的一部分,比如用 MAP_FIXED
来“偷”一个 MADV_FREE
的段,然后立即取消映射。)
但是大多数程序并不是直接从内存页面中分配东西;它们使用的是 malloc
风格的分配器。当你调用 free
时,分配器只能在你恰好释放了映射中的最后一个活对象(或者在数据段的最后 N 页中)时,才能将页面释放给操作系统。你的应用程序无法合理预测这一点,甚至无法提前检测到它的发生。
CPython 让事情变得更加复杂——它在一个自定义的内存分配器之上有一个自定义的两级对象分配器。(想了解更多,可以查看 源代码注释。)而且,即使在 C API 层面,更不用说 Python,你也无法直接控制顶级对象何时被释放。
所以,当你释放一个对象时,怎么知道它是否会将内存释放给操作系统呢?首先,你得知道你已经释放了最后一个引用(包括你不知道的任何内部引用),这样垃圾回收器才能释放它。(与其他实现不同,至少 CPython 会在允许的情况下立即释放对象。)这通常会释放下一级的至少两个东西(例如,对于一个字符串,你释放了 PyString
对象和字符串缓冲区)。
如果你确实释放了一个对象,要知道这是否导致下一级释放一个对象存储块,你必须了解对象分配器的内部状态,以及它是如何实现的。(显然,只有在你释放了块中的最后一个东西时,这种情况才会发生,即使如此,它也可能不会发生。)
如果你确实释放了一个对象存储块,要知道这是否导致了 free
调用,你必须了解 PyMem 分配器的内部状态,以及它是如何实现的。(同样,你必须释放在 malloc
区域内的最后一个正在使用的块,即使如此,它也可能不会发生。)
如果你确实 free
了一个 malloc
的区域,要知道这是否导致了 munmap
或类似的操作(或 brk
),你必须了解 malloc
的内部状态,以及它是如何实现的。而且这一点,与其他情况不同,是高度依赖平台的。(同样,你通常必须释放在 mmap
段内的最后一个正在使用的 malloc
,即使如此,它也可能不会发生。)
所以,如果你想理解为什么它释放了正好 50.5MB 的内存,你需要从底层开始追踪。为什么在你进行一个或多个 free
调用时,malloc
会取消映射 50.5MB 的页面(可能比 50.5MB 多一点)?你需要查看你平台的 malloc
,然后查看各种表和列表以了解它的当前状态。(在某些平台上,它甚至可能会利用系统级的信息,这几乎不可能在不拍摄系统快照的情况下捕获,但幸运的是,这通常不是问题。)然后你还得在上面的三个层次上做同样的事情。
所以,回答这个问题唯一有用的答案就是“因为。”
除非你在做资源受限的开发(例如嵌入式开发),否则你没有理由关心这些细节。
如果你确实在做资源受限的开发,了解这些细节是没用的;你几乎必须绕过所有这些层次,特别是在应用层面上 mmap
你需要的内存(可能中间有一个简单、易懂的、特定于应用的区域分配器)。
我猜你真正关心的问题是:
有没有办法强制Python释放所有使用过的内存(如果你知道之后不会再用这么多内存)?
没有,没办法。不过有个简单的解决办法:使用子进程。
比如说,你需要500MB的临时存储来处理5分钟的工作,但之后你还要运行2个小时,并且再也不会用到这么多内存。这时候可以启动一个子进程来完成那些占用内存较大的工作。当这个子进程结束后,内存就会被释放。
这并不是完全简单和免费的,但相对来说还是很容易和便宜的,通常这样的交换是值得的。
首先,创建子进程最简单的方法是用concurrent.futures
(如果你用的是3.1及更早的版本,可以用futures
这个库):
with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
result = executor.submit(func, *args, **kwargs).result()
如果你需要更多的控制,可以使用multiprocessing
模块。
使用子进程的成本包括:
- 在某些平台上,启动进程的速度比较慢,特别是在Windows上。这里说的是毫秒级别,而不是分钟。如果你启动一个子进程来处理300秒的工作,你根本不会注意到这个延迟。但这并不是免费的。
- 如果你使用的临时内存真的很大,这样做可能会导致你的主程序被换出内存。当然,从长远来看,你是节省了时间,因为如果那块内存一直占着,最终也会导致换出内存。但在某些情况下,这可能会让逐渐变慢的情况突然变得非常明显(而且是提前出现的延迟)。
- 在进程之间发送大量数据可能会很慢。同样,如果你只是发送2K的参数并返回64K的结果,你可能不会注意到,但如果你发送和接收大量数据,就需要使用其他机制(比如文件、
mmap
,或者使用multiprocessing
中的共享内存API等)。 - 在进程之间发送大量数据意味着这些数据必须是可序列化的(或者,如果你把它们放在文件或共享内存中,必须是可结构化的,理想情况下是可用
ctypes
处理的)。
在堆上分配的内存可能会受到高水位线的影响。这是因为Python在分配小对象时有一些内部优化(使用PyObject_Malloc
),它会把内存分成4 KiB的小池子,这些池子是按照8字节的倍数来分类的,最大可以到256字节(在3.3版本中是512字节)。这些池子本身又是在256 KiB的区域里,所以如果其中一个池子里有一个块被使用了,整个256 KiB的区域就不会被释放。在Python 3.3中,小对象的分配器改成了使用匿名内存映射,而不是堆,这样在释放内存时应该会表现得更好。
此外,Python内置的类型还会维护一些之前分配过的对象的空闲列表,这些对象可能会使用小对象分配器,也可能不会。比如int
类型就有自己的空闲列表,清理这个列表需要调用PyInt_ClearFreeList()
。你可以通过执行完整的gc.collect
间接地调用它。
试试这样做,看看你得到什么。这里有一个关于psutil.Process.memory_info的链接。
import os
import gc
import psutil
proc = psutil.Process(os.getpid())
gc.collect()
mem0 = proc.memory_info().rss
# create approx. 10**7 int objects and pointers
foo = ['abc' for x in range(10**7)]
mem1 = proc.memory_info().rss
# unreference, including x == 9999999
del foo, x
mem2 = proc.memory_info().rss
# collect() calls PyInt_ClearFreeList()
# or use ctypes: pythonapi.PyInt_ClearFreeList()
gc.collect()
mem3 = proc.memory_info().rss
pd = lambda x2, x1: 100.0 * (x2 - x1) / mem0
print "Allocation: %0.2f%%" % pd(mem1, mem0)
print "Unreference: %0.2f%%" % pd(mem2, mem1)
print "Collect: %0.2f%%" % pd(mem3, mem2)
print "Overall: %0.2f%%" % pd(mem3, mem0)
输出:
Allocation: 3034.36%
Unreference: -752.39%
Collect: -2279.74%
Overall: 2.23%
编辑:
我改成了相对于进程虚拟内存大小来测量,以消除系统中其他进程的影响。
C运行时(比如glibc,msvcrt)会在顶部的连续空闲空间达到一个固定的、动态的或可配置的阈值时缩小堆。在glibc中,你可以通过mallopt
(M_TRIM_THRESHOLD)来调整这个阈值。因此,如果堆缩小的幅度比你free
的块要大,甚至大得多,这也不奇怪。
在3.x版本中,range
不会创建一个列表,所以上面的测试不会创建1000万个int
对象。即使它创建了,3.x版本的int
类型基本上就是2.x版本的long
,而且它并没有实现空闲列表。