3

我目前正在用 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 时,它会崩溃,但会再次开始消耗内存。

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

4

2 回答 2

9

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


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

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

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

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


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


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

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


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

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

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

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

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

于 2013-06-27T08:46:20.183 回答
0

节省 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++ 摇滚!!!

于 2020-09-16T08:14:54.737 回答