Python - 处理内存泄漏问题

32

我有一个Python程序,运行一系列实验,每次测试之间都没有存储数据。我的代码包含了一个内存泄漏问题,但我完全找不到(我已经看过关于内存泄漏的其他帖子)。由于时间限制,我不得不放弃找到内存泄漏问题,但如果我能够隔离每个实验,程序可能会运行足够长的时间来产生我需要的结果。

  • 在单独的线程中运行每个测试是否有帮助?
  • 除此之外,还有其他隔离泄漏效果的方法吗?

关于特定情况的详细信息

  • 我的代码分为两部分:实验执行器和实际实验代码。
  • 虽然实验运行的所有代码和每个实验使用的代码之间没有共享全局变量,但某些类/函数是必须共享的。
  • 实验执行器不仅仅是一个简单的for循环,可以轻松地放入shell脚本中。它首先根据配置参数决定需要运行哪些测试,然后运行测试,最后以特定方式输出数据。
  • 我尝试手动调用垃圾回收器,以防问题只是垃圾回收器没有运行,但这并没有起作用。

更新

Gnibbler提供的答案实际上让我发现了我的ClosenessCalculation对象没有被释放,它们存储了每次计算期间使用的所有数据。然后我使用这个方法手动删除了一些链接,似乎解决了内存问题。


2
在Python中定义"内存泄漏"。 - hasen
我的意思是,你不可能“忘记”释放任何内存;它会被垃圾回收。 - hasen
2
它不仅增长,而且还在不断变大。 - Casebash
你应该接受gnibbler的答案。这不仅是一个好答案,它实际上解决了你的问题! - steveha
3
你可以在几乎任何编程语言中出现“内存泄漏”:即使是带有GC的语言也一样——它只是由于未回收先前分配而导致可用内存的减少(在启用GC的语言中,这意味着[越来越多的]对象永远不会被标记为可回收)。没错,GC通过消除巨大的负担使编程更容易,但它并不能消除糟糕的代码或设计。 - user166390
显示剩余8条评论
4个回答

69
你可以使用类似这样的方法来帮助追踪内存泄漏。
>>> 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个列表的列表


1
哇,这非常有用。虽然,这确实应该放在如何找到内存泄漏的线程之一上。 - Casebash
1
在比较对象之前进行垃圾回收是否值得? - Casebash
1
谢谢!如果结合 id() 函数,使用这个线程 https://dev59.com/CXM_5IYBdhLWcg3wZSM6 ,你可以轻松地获取泄露的对象! - Alexander A.Sosnovskiy
非常感谢您!这节省了我很多时间和精力,让我感到非常宽慰 :-) - raphael

4
线程不能解决问题。如果你必须放弃找到泄漏点,那么唯一的解决办法是定期运行一个新的进程(例如,当测试留下的内存消耗过高时,您可以通过读取Linux中的/ proc / self / status以及其他类似方法来轻松确定VM大小,然后启动新的进程)。
确保整个脚本采用可选参数告诉它从哪个测试编号(或其他测试标识)开始,这样当脚本的一个实例决定它占用了太多内存时,它可以告诉其后继者从哪里重新开始。
或者更可靠地,确保每个测试完成后,它的标识被附加到某个具有公认名称的文件中。当程序启动时,它会首先读取该文件,从而知道已经运行了哪些测试。这种架构更加可靠,因为它还涵盖了程序在测试期间崩溃的情况;当然,要完全自动化从这些崩溃中恢复,您需要一个单独的看门狗程序和进程来负责在确定前一个程序崩溃时启动测试程序的新实例(它可以使用subprocess进行此操作--它还需要一种方法来确定序列何时结束,例如,测试程序的正常退出可能意味着需要启动新的新实例,而任何崩溃或状态!= 0的退出都表示需要启动新的新实例)。
如果这些架构吸引您,但您需要进一步帮助来实现它们,请在此答案中发表评论,我将很乐意提供示例代码--我不想“预先”执行它,以防有尚未表达的问题使架构对您不适用。 (知道您需要在哪些平台上运行也可能有所帮助)。

3

我曾经遇到一个第三方的C库也存在内存泄漏问题。最干净的解决方法是使用fork和wait。它的优点在于每个运行后不必创建单独的进程,你可以定义批处理的大小。

这里是一个通用的解决方案(如果你发现了内存泄漏,唯一需要做的更改就是将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 变量的值更多的次数。


2
我会将实验重构为单独的函数(如果还没有这样做的话),然后从命令行接受一个实验编号,调用单个实验函数。

接下来,只需编写以下脚本:
#!/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 脚本,但不幸的是,我的实验代码比那复杂得多。 - Casebash
1
@paxdiablo:我同意这个答案应该留下来,因为它可能对其他访问这个问题的人有帮助。 - Casebash

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