【问题标题】:Memory leaks when image discarded in Python在 Python 中丢弃图像时内存泄漏
【发布时间】:2013-06-27 08:13:17
【问题描述】:

我目前正在用 Python 编写一个简单的棋盘游戏,我刚刚意识到垃圾收集不会在重新加载图像时从内存中清除丢弃的位图数据。它仅在游戏启动或加载或分辨率发生变化时发生,但它会成倍消耗内存,所以我不能让这个问题得不到解决。

重新加载图像时,所有引用都会转移到新的图像数据,因为它与原始图像数据绑定到的变量相同。我尝试使用 collect() 强制垃圾回收,但没有帮助。

我写了一个小样本来演示我的问题。

from tkinter import Button, DISABLED, Frame, Label, NORMAL, Tk
from PIL.Image import open
from PIL.ImageTk import PhotoImage

class App(Tk):
    def __init__(self):
        Tk.__init__(self)
        self.text = Label(self, text = "Please check the memory usage. Then push button #1.")
        self.text.pack()
        self.btn = Button(text = "#1", command = lambda : self.buttonPushed(1))
        self.btn.pack()

    def buttonPushed(self, n):
        "Cycle to open the Tab module n times."
        self.btn.configure(state = DISABLED) # disable to prevent paralell cycles
        if n == 100:
            self.text.configure(text = "Overwriting the bitmap with itself 100 times...\n\nCheck the memory usage!\n\nUI may seem to hang but it will finish soon.")
            self.update_idletasks()
        for i in range(n):      # creates the Tab frame whith the img, destroys it, then recreates them to overwrite the previous Frame and prevous img
            b = Tab(self)
            b.destroy()
            if n == 100:
                print(i+1,"percent of processing finished.")
        if n == 1:
            self.text.configure(text = "Please check the memory usage now.\nMost of the difference is caused by the bitmap opened.\nNow push button #100.")
            self.btn.configure(text = "#100", command = lambda : self.buttonPushed(100))
        self.btn.configure(state = NORMAL)  # starting cycles is enabled again       

class Tab(Frame):
    """Creates a frame with a picture in it."""
    def __init__(self, master):
        Frame.__init__(self, master = master)
        self.a = PhotoImage(open("map.png"))    # img opened, change this to a valid one to test it
        self.b = Label(self, image = self.a)
        self.b.pack()                           # Label with img appears in Frame
        self.pack()                             # Frame appears

if __name__ == '__main__':
    a = App()

要运行上面的代码,您需要一个 PNG 图像文件。我的 map.png 的尺寸是 1062×1062。作为 PNG,它是 1.51 MB,作为位图数据,它大约是 3-3.5 MB。使用大图轻松查看内存泄漏。

运行我的代码时的预期结果:python 的进程一个周期地消耗内存。当它消耗大约 500 MB 时,它会崩溃,但会再次开始消耗内存。

请给我一些建议如何解决这个问题。我很感激每一个帮助。谢谢你。提前。

【问题讨论】:

  • 首先,消耗500MB真的有问题吗?就此而言,500MB 只是虚拟内存还是物理/常驻内存? Python 通常不会将内存返回给操作系统;当您以后需要它时,它会保留它以供重复使用。这通常会使事情变得更快——一遍又一遍地分配、释放和重新分配数十 MB 需要大量时间。另外,你是在什么平台上的?例如,在 64 位 OS X 上,大多数进程以数百 MB 的 VM 结束,而在 32 位 linux 上,这种情况要少得多。
  • 我不知道是物理内存还是 VRAM。我对编程很陌生,我不知道有什么工具可以检查。你能推荐一些吗?我使用命令行和任务管理器中的任务列表来跟踪内存消耗。我的操作系统是 Win7 x64。所以你说,只要偶尔崩溃就不是问题了?那将是一种解脱。
  • TaskManager 显示物理和虚拟内存的单独数字,但我不记得它们的确切名称。无论如何,如果您认为可能存在实际问题,则必须先了解 Windows 中的内存管理是如何工作的,然后才能进行调查。如果你没有任何实际问题,只是担心而不知道为什么,那就别担心了。

标签: python memory-leaks python-3.x tkinter pillow


【解决方案1】:

首先,您绝对没有内存泄漏。如果它在接近 500MB 时“崩溃”并且从未超过它,则它不可能泄漏。


我猜你根本没有任何问题。

当 Python 的垃圾收集器清理东西时(这通常在您在 CPython 中完成后立即发生),它通常不会真正将内存释放给操作系统。相反,它会保留它以防您以后需要它。这是有意为之的——除非你正在破坏交换,否则重用内存要比不断释放和重新分配它快得多。

另外,如果 500MB 是虚拟内存,那在现代 64 位平台上算不了什么。如果它没有映射到物理/常驻内存(或者如果计算机空闲则被映射,否则很快被扔掉),这不是问题;只是操作系统很好地利用了实际上免费的资源。

更重要的是:是什么让您认为有问题?是否有任何实际症状,或者只是程序管理器/活动监视器/顶部/任何让你害怕的东西? (如果是后者,请查看其他程序。在我的 Mac 上,我有 28 个程序当前正在使用超过 400MB 的虚拟内存运行,并且我正在使用 16GB 中的 11 个,即使少于 3GB 是实际上是连线的。比如说,如果我启动 Logic,内存的收集速度将超过 Logic 使用它的速度;在此之前,操作系统为什么要浪费精力取消映射内存(尤其是当它无法确保某些进程不会去求那块以后没用的内存)?


但如果存在一个真正的问题,有两种方法可以解决它。


第一个技巧是在子进程中执行所有内存密集型操作,您可以杀死并重新启动以恢复临时内存(例如,使用multiprocessing.Processconcurrent.futures.ProcessPoolExecutor)。

这通常会使事情变得更慢而不是更快。当临时内存主要是直接进入 GUI 的东西,因此必须存在于主进程中时,这显然并不容易。


另一种选择是找出内存的使用位置,而不是同时保留这么多对象。基本上,这有两个部分:

首先,在每个事件处理程序结束之前释放所有可能的东西。这意味着在文件上调用 closedeling 对象或将所有对它们的引用设置为 None,在不可见的 GUI 对象上调用 destroy,最重要的是,不存储对事物的引用你不需要。 (你真的需要在使用后保留PhotoImage吗?如果需要,有什么方法可以按需加载图像吗?)

接下来,确保您没有引用循环。在 CPython 中,只要没有循环,垃圾就会立即清理——但如果有,它们会一直存在,直到循环检查器运行。您可以使用gc module 对此进行调查。一件非常快速的事情是经常尝试一下:

print(gc.get_count())
gc.collect()
print(gc.get_count())

如果您看到大量下降,则说明您有周期。您必须查看gc.getobjects()gc.garbage 内部,或者附加回调,或者只是对您的代码进行推理以准确找到循环的位置。对于每一个,如果你真的不需要双向引用,就去掉一个;如果这样做,请将其中一个更改为 weakref

【讨论】:

  • 感谢您的回答。我很担心,因为我注意尽可能少地使用资源,但它仍然在不断增长。现在我看到它没问题。我也很感谢您的问题解决提示。如果我有真正的记忆问题,它们会派上用场。 :)
【解决方案2】:

节省 500MB 值得,节省 100MB 值得,节省 10MB 值得。 内存有黄金的价格,许多人建议浪费它。 当然,这是你的决定,如果你想把它浪费在你的 Mac 上,那就去做吧...... 当然,如何编写非常糟糕的软件是非常可悲的建议。

使用https://pypi.org/project/memory-profiler/ 跟踪您的 Python 内存分配。 使用

x = someRamConsumingObject()
# do the stuff here ...
# remove the refrences
del x
x = None
gc.Collect() # try to force garbage collector to collect

远离哲学讨论,来自工业边缘计算的真实示例为我们提供了需要改进的确切理由。如果在容器中运行 Python,很快就会碰壁,尤其是在 Edge 上运行多个容器并承受繁重的生产负载。

即使 Edge 有 16GiB,你也很快就会碰壁,尤其是使用 Pandas 等数据分析工具。

那么,我的朋友,你会明白什么是垃圾收集器,什么是“内存不受控制”。

C++ 摇滚!!!

【讨论】:

    猜你喜欢
    • 2016-08-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-21
    • 1970-01-01
    • 1970-01-01
    • 2016-08-21
    • 2016-04-12
    相关资源
    最近更新 更多