【问题标题】:Releasing memory in Python在 Python 中释放内存
【发布时间】:2013-03-05 12:05:53
【问题描述】:

我有几个关于以下示例中的内存使用的相关问题。

  1. 如果我在解释器中运行,

    foo = ['bar' for _ in xrange(10000000)]
    

    我机器上使用的实际内存达到80.9mb。那我,

    del foo
    

    实际内存下降,但仅限于30.4mb。解释器使用4.4mb 基线,那么不向操作系统释放26mb 内存有什么好处?是不是因为 Python 在“提前计划”,认为你可能会再次使用那么多内存?

  2. 为什么要特别发布50.5mb - 发布的数量是多少?

  3. 有没有办法强制 Python 释放所有已使用的内存(如果您知道不会再使用那么多内存)?

注意 这个问题不同于How can I explicitly free memory in Python? 因为即使在解释器通过垃圾收集释放对象之后(使用或不使用gc.collect),这个问题主要涉及从基线增加的内存使用量。

【问题讨论】:

  • 值得注意的是,这种行为并非特定于 Python。通常情况下,当一个进程释放一些堆分配的内存时,内存不会被释放回操作系统,直到进程终止。
  • 您的问题涉及多个问题——其中一些是重复的,其中一些不适合 SO,其中一些可能是好问题。你是在问 Python 是否不释放内存,在什么情况下它可以/不可以,底层机制是什么,为什么要这样设计,是否有任何变通方法,或者完全是其他什么?
  • @abarnert 我结​​合了相似的子问题。回答您的问题:我知道 Python 会向操作系统释放一些内存,但为什么不释放全部内存以及为什么会释放这么多内存。如果在某些情况下它不能,为什么?还有什么解决方法。
  • @jww 我不这么认为。这个问题真的与为什么解释器进程在调用gc.collect 完全收集垃圾之后从未释放内存有关。

标签: python memory-management


【解决方案1】:

在堆上分配的内存可能会受到高水位标记的影响。这因 Python 的内部优化在 4 KiB 池中分配小对象 (PyObject_Malloc) 而变得复杂,分配大小分类为 8 字节的倍数 - 最多 256 字节(3.3 中为 512 字节)。池本身在 256 KiB arena 中,因此如果只使用一个池中的一个块,则不会释放整个 256 KiB arena。在 Python 3.3 中,小对象分配器被切换为使用匿名内存映射而不是堆,因此它应该在释放内存方面表现更好。

此外,内置类型维护先前分配的对象的空闲列表,这些对象可能使用也可能不使用小对象分配器。 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%

编辑:

我切换到相对于进程 VM 大小进行测量,以消除系统中其他进程的影响。

当顶部的连续可用空间达到恒定、动态或可配置的阈值时,C 运行时(例如 glibc、msvcrt)会缩小堆。使用 glibc,您可以使用 mallopt (M_TRIM_THRESHOLD) 对其进行调整。鉴于此,如果堆比您free 的块收缩更多——甚至更多——也就不足为奇了。

在 3.x 中,range 不会创建列表,因此上面的测试不会创建 1000 万个 int 对象。即使是这样,3.x 中的int 类型基本上也是2.x 的long,它没有实现freelist。

【讨论】:

  • 使用memory_info() 代替get_memory_info() 并且定义了x
  • 即使在 Python 3 中,您确实会得到 10^7 ints,但每个都替换循环变量中的最后一个,因此它们不会同时存在。
  • 我遇到了内存泄漏问题,我猜你在这里回答的原因。但是我怎么能证明我的猜测呢?是否有任何工具可以显示许多池已分配,但只使用了一个小块?
【解决方案2】:

首先,您可能需要安装 Glances:

sudo apt-get install python-pip build-essential python-dev lm-sensors 
sudo pip install psutil logutils bottle batinfo https://bitbucket.org/gleb_zhulik/py3sensors/get/tip.tar.gz zeroconf netifaces pymdstat influxdb elasticsearch potsdb statsd pystache docker-py pysnmp pika py-cpuinfo bernhard
sudo pip install glances

然后在终端运行它!

glances

在您的 Python 代码中,在文件开头添加以下内容:

import os
import gc # Garbage Collector

使用“Big”变量(例如:myBigVar)后,您想为其释放内存,在您的 python 代码中写入以下内容:

del myBigVar
gc.collect()

在另一个终端中,运行你的 python 代码并在“glances”终端中观察你的系统是如何管理内存的!

祝你好运!

附:我假设您正在使用 Debian 或 Ubuntu 系统

【讨论】:

    【解决方案3】:

    eryksun 已经回答了问题 #1,我已经回答了问题 #3(原来的 #4),但现在让我们回答问题 #2:

    为什么它特别释放 50.5mb - 释放的数量是多少?

    它最终基于的是 Python 和 malloc 内部的一系列巧合,这些巧合很难预测。

    首先,根据您测量内存的方式,您可能只测量实际映射到内存中的页面。在这种情况下,任何时候页面被分页器换出,内存都会显示为“已释放”,即使它还没有被释放。

    或者您可能正在测量正在使用的页面,这可能会或可能不会计算已分配但从未触及的页面(在乐观过度分配的系统上,如 linux),已分配但标记为 MADV_FREE 的页面,等等

    如果您确实在测量已分配的页面(这实际上并不是一件非常有用的事情,但这似乎是您要问的问题),并且页面确实已被释放,这可能发生在两种情况下:要么您使用brk 或等效的方法来缩小数据段(现在非常少见),或者您已使用munmap 或类似方法来释放映射的段。 (理论上,后者还有一个较小的变体,因为有一些方法可以释放映射段的一部分——例如,用MAP_FIXED 窃取它以获得您立即取消映射的MADV_FREE 段。)

    但是大多数程序不会直接从内存页面中分配东西;他们使用malloc 风格的分配器。当您调用free 时,如果您恰好是free 映射中的最后一个活动对象(或数据段的最后N 页中),则分配器只能将页面释放到操作系统。您的应用程序无法合理地预测这一点,甚至无法提前检测到它发生了。

    CPython 使这变得更加复杂——它在 malloc 之上的自定义内存分配器之上有一个自定义的 2 级对象分配器。 (有关更详细的解释,请参阅 the source comments。)最重要的是,即使在 C API 级别,更不用说 Python,您甚至无法直接控制何时释放顶级对象。

    那么,当你释放一个对象时,你怎么知道它是否会释放内存给操作系统呢?嗯,首先你必须知道你已经释放了最后一个引用(包括你不知道的任何内部引用),允许 GC 释放它。 (与其他实现不同,至少 CPython 会在允许时立即释放对象。)这通常会在下一层释放至少两件事(例如,对于字符串,您正在释放 PyString 对象,并且字符串缓冲区)。

    如果你释放一个对象,要知道这是否会导致下一级释放一个对象存储块,你必须知道对象分配器的内部状态,以及如何它已实施。 (除非你释放块中的最后一个东西,否则它显然不会发生,即使那样,它也可能不会发生。)

    如果你释放一个对象存储块,要知道这是否会导致free 调用,你必须知道 PyMem 分配器的内部状态,以及它是如何实现的。 (同样,您必须在 malloced 区域内释放最后一个正在使用的块,即使那样,它也可能不会发生。)

    如果你freemalloced 区域,要知道这是否会导致munmap 或等效(或brk),你必须知道内部状态malloc,以及它是如何实现的。与其他的不同,这个是高度特定于平台的。 (同样,您通常必须在 mmap 段中释放最后一个使用中的 malloc,即使那样,它也可能不会发生。)

    所以,如果您想了解为什么它恰好释放了 50.5mb,您将不得不从下往上追踪它。为什么malloc 在您进行一次或多次free 调用时取消映射价值50.5mb 的页面(可能超过50.5mb)?您必须阅读您平台的malloc,然后遍历各种表格和列表以查看其当前状态。 (在某些平台上,它甚至可能使用系统级信息,如果不制作系统快照以进行离线检查,几乎不可能捕获这些信息,但幸运的是,这通常不是问题。)然后你必须在上面的 3 个级别上做同样的事情。

    因此,对这个问题唯一有用的答案是“因为”。

    除非您正在进行资源有限(例如嵌入式)开发,否则您没有理由关心这些细节。

    如果您正在进行资源有限的开发,那么了解这些细节是没有用的;您几乎必须围绕所有这些级别进行最终运行,特别是 mmap 应用程序级别所需的内存(可能在两者之间使用一个简单的、易于理解的、特定于应用程序的区域分配器)。

    【讨论】:

      【解决方案4】:

      我猜你在这里真正关心的问题是:

      有没有办法强制 Python 释放所有已使用的内存(如果您知道不会再使用那么多内存)?

      不,没有。但是有一个简单的解决方法:子进程。

      如果您需要 500MB 的临时存储 5 分钟,但之后您需要再运行 2 小时并且不会再接触那么多内存,请生成一个子进程来执行内存密集型工作。当子进程消失时,内存被释放。

      这并不是完全简单和免费的,但它非常简单且便宜,通常足以让交易变得有价值。

      首先,创建子进程的最简单方法是使用 concurrent.futures(或者,对于 3.1 及更早版本,使用 PyPI 上的 futures 反向端口):

      with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
          result = executor.submit(func, *args, **kwargs).result()
      

      如果您需要更多控制,请使用multiprocessing 模块。

      费用为:

      • 在某些平台上,进程启动有点慢,尤其是 Windows。我们在这里说的是毫秒,而不是分钟,如果你让一个孩子做 300 秒的工作,你甚至不会注意到它。但它不是免费的。
      • 如果您使用的大量临时内存确实很大,这样做可能会导致您的主程序被换出。当然,从长远来看,您可以节省时间,因为如果该内存永远存在,它将不得不在某个时候导致交换。但这会在某些用例中将逐渐的缓慢转变为非常明显的一次性(和早期)延迟。
      • 在进程之间发送大量数据可能会很慢。同样,如果您正在谈论发送超过 2K 的参数并返回 64K 的结果,您甚至不会注意到它,但如果您正在发送和接收大量数据,您将需要使用其他一些机制(一个文件,mmapped 或其他;multiprocessing 中的共享内存 API;等等)。
      • 在进程之间发送大量数据意味着数据必须是可腌制的(或者,如果将它们保存在文件或共享内存中,struct-able 或理想情况下是 ctypes-able)。

      【讨论】:

      • 非常好的技巧,虽然没有解决问题:(但我真的很喜欢它
      • 这是唯一对我有用的解决方案,因为使用 delgc.collect() 没有。
      猜你喜欢
      • 1970-01-01
      • 2016-05-22
      • 2023-04-05
      • 2012-08-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-14
      相关资源
      最近更新 更多