Python - 解决内存泄漏问题
Posted
技术标签:
【中文标题】Python - 解决内存泄漏问题【英文标题】:Python - Working around memory leaks 【发布时间】:2010-12-11 02:31:26 【问题描述】:我有一个 Python 程序,它运行一系列实验,没有打算将数据从一个测试存储到另一个测试。我的代码包含一个我完全找不到的内存泄漏(我查看了关于内存泄漏的other threads)。由于时间限制,我不得不放弃寻找泄漏,但如果我能够隔离每个实验,该程序可能会运行足够长的时间来产生我需要的结果。
在单独的线程中运行每个测试会有帮助吗? 还有其他方法可以隔离泄漏的影响吗?具体情况详解
我的代码有两部分:实验运行器和实际实验代码。 虽然运行所有实验的代码和每个实验使用的代码之间没有共享全局变量,但某些类/函数必须共享。 实验运行器不仅仅是一个可以轻松放入 shell 脚本的简单 for 循环。它首先根据配置参数决定需要运行的测试,然后运行测试,然后以特定方式输出数据。 我尝试手动调用垃圾收集器,以防问题仅仅是没有运行垃圾收集,但这不起作用更新
Gnibbler 的回答实际上让我发现我的 ClosenessCalculation 对象存储了所有在每次计算期间使用的数据并没有被杀死。然后我用它手动删除了一些似乎已经解决了内存问题的链接。
【问题讨论】:
在python中定义“内存泄漏”。 我的意思是,你不可能“忘记”释放任何内存;它是 GCed。 你怎么知道你有内存泄漏?是不是您的进程内存增长到很大,并且从不缩小?如果是这样,请注意 Python 不一定会因为不再使用内存而将内存返回给操作系统。 它不仅在增长,而且还在不断变大 您几乎可以在任何编程语言中出现“内存泄漏”:即使是带有 GC 的语言——这只是由于缺乏回收而导致的可用内存减少以前的分配(在支持 GC 的语言中,这仅仅意味着 [和越来越多的] 对象永远不会有资格进行回收)。没错,GC 通过消除巨大的负担让编程变得更容易,但它并没有消除糟糕的代码或糟糕的设计。 【参考方案1】:您可以使用类似的方法来帮助追踪内存泄漏
>>> from collections import defaultdict
>>> from gc import get_objects
>>> before = defaultdict(int)
>>> after = defaultdict(int)
>>> for i in get_objects():
... before[type(i)] += 1
...
现在假设测试泄漏了一些内存
>>> leaked_things = [[x] for x in range(10)]
>>> for i in get_objects():
... after[type(i)] += 1
...
>>> print [(k, after[k] - before[k]) for k in after if after[k] - before[k]]
[(<type 'list'>, 11)]
11 因为我们泄露了一个列表,其中包含另外 10 个列表
【讨论】:
哇,这非常有用。虽然,这确实属于我如何找到内存泄漏线程之一 在比较对象之前是否值得进行垃圾回收? 谢谢!如果将此与 id() 函数结合使用并使用此线程 ***.com/questions/1396668/python-get-object-by-id 您可以轻松获取泄漏对象!【参考方案2】:我在第三方 C 库泄漏时遇到了同样的问题。我能想到的最干净的解决方法是分叉并等待。它的优点是您甚至不必在每次运行后创建单独的进程。您可以定义批次的大小。
这是一个通用的解决方案(如果您发现泄漏,您需要做的唯一更改是将 run() 更改为调用 run_single_process() 而不是 run_forked(),然后您就完成了):
import os,sys
batchSize = 20
class Runner(object):
def __init__(self,dataFeedGenerator,dataProcessor):
self._dataFeed = dataFeedGenerator
self._caller = dataProcessor
def run(self):
self.run_forked()
def run_forked(self):
dataFeed = self._dataFeed
dataSubFeed = []
for i,dataMorsel in enumerate(dataFeed,1):
if i % batchSize > 0:
dataSubFeed.append(dataMorsel)
else:
self._dataFeed = dataSubFeed
self.fork()
dataSubFeed = []
if self._child_pid is 0:
self.run_single_process()
self.endBatch()
def run_single_process(self)
for dataMorsel in self._dataFeed:
self._caller(dataMorsel)
def fork(self):
self._child_pid = os.fork()
def endBatch(self):
if self._child_pid is not 0:
os.waitpid(self._child_pid, 0)
else:
sys.exit() # exit from the child when done
这隔离了子进程的内存泄漏。而且它的泄漏次数永远不会超过 batchSize 变量的值。
【讨论】:
【参考方案3】:我会简单地将实验重构为单独的函数(如果不是那样的话),然后从调用单个实验函数的命令行接受一个实验编号。
刚刚好的一个shell脚本如下:
#!/bin/bash
for expnum in 1 2 3 4 5 6 7 8 9 10 11 ; do
python youProgram $expnum otherParams
done
这样,您可以将大部分代码保持原样,这将清除您认为在每次实验之间存在的任何内存泄漏。
当然,最好的解决方案总是找到并解决问题的根本原因,但正如您已经说过的,这不是您的选择。
尽管在 Python 中很难想象内存泄漏,但我会相信你的话——不过,你可能至少要考虑一下你在那里弄错了的可能性。考虑在一个单独的问题中提出那个,这是我们可以以低优先级处理的事情(与这个快速修复版本相反)。
更新:制作社区 wiki,因为问题与原来的有所不同。我会删除答案,但事实上我仍然认为它很有用 - 你可以对你的实验运行者做同样的事情,就像我提出 bash 脚本一样,你只需要确保实验是单独的进程,这样就不会发生内存泄漏(如果内存泄漏发生在运行程序中,您将不得不进行根本原因分析并正确修复错误)。
【讨论】:
我确实考虑过只写一个shell脚本,但不幸的是我的实验代码比这复杂得多 @paxdiablo:我同意应该留下这个答案,因为它可能对访问这个问题的其他人有所帮助【参考方案4】:线程无济于事。如果您必须放弃寻找泄漏,那么控制其影响的唯一解决方案是偶尔运行一个新的进程(例如,当测试导致整体内存消耗过高而无法满足您的喜好时-- 您可以通过阅读 Linux 中的/proc/self/status
以及其他操作系统上的其他类似方法轻松确定 VM 大小。
确保整个脚本采用一个可选参数来告诉它从哪个测试编号(或其他测试标识)开始,这样当脚本的一个实例决定它占用太多内存时,它可以告诉它的后继者在哪里重新开始。
或者,更确切地说,确保在完成每个测试后,其标识会附加到某个具有众所周知的名称的文件中。当程序启动时,它首先读取该文件,因此知道已经运行了哪些测试。这种架构更加稳固,因为它还涵盖了程序在测试期间崩溃的情况;当然,要从此类崩溃中完全自动恢复,您需要一个单独的看门狗程序和进程来负责在确定前一个已崩溃时启动测试程序的新实例(它可以使用subprocess
目的——它还需要一种方法来判断序列何时完成,例如,正常退出测试程序可能意味着任何崩溃或状态为 != 0 的退出都表示需要启动一个新的实例)。
如果这些架构很有吸引力,但您需要进一步的帮助来实现它们,只需对此答案发表评论,我很乐意提供示例代码——我不想“抢先”这样做,以防万一-使架构不适合您的未表达的问题。 (了解您需要在哪些平台上运行也可能会有所帮助。
【讨论】:
以上是关于Python - 解决内存泄漏问题的主要内容,如果未能解决你的问题,请参考以下文章
从 python 中的共享库返回的数组 - 这是内存泄漏吗?