我目前正在用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时,它会崩溃,但会再次开始耗尽内存。
请给我一些建议如何解决这个问题。我很感激每一个帮助。谢谢。提前。
答案 0 :(得分:8)
首先,你肯定没有内存泄漏。如果它在接近500MB并且从未穿过它时“崩溃”,它就不会泄漏。
我的猜测是你根本没有任何问题。
当Python的垃圾收集器清理时(通常在CPython中完成它时会立即发生),它通常不会将内存释放到操作系统。相反,它会保留它,以防您以后需要它。这是故意的 - 除非你是颠倒交换,重用内存要快得多,而不是继续释放和重新分配它。
此外,如果500MB是虚拟内存,那么现代64位平台上什么都不是。如果它没有映射到物理/驻留内存(或者如果计算机处于空闲状态但是很快就会被映射,则会映射),这不是问题;它只是操作系统很好的资源,实际上是免费的。
更重要的是:是什么让你觉得有问题?是否有任何实际症状,或只是程序管理器/活动监视器/顶部/什么吓到你? (如果是后者,请看看其他程序。在我的Mac上,我有28个程序当前运行使用超过400MB的虚拟内存,我使用的是16GB中的11个,即使少于3GB是实际上是有线的。如果我说,启动Logic,内存的收集速度会比Logic可以使用的更快;直到那时,为什么操作系统会浪费工作来取消内存(特别是当它无法确定某些进程不会去问那个以后没用过的记忆吗?
但如果 是一个真正的问题,有两种方法可以解决它。
第一个技巧是在子进程中执行所有内存密集型操作,您可以终止并重新启动以恢复临时内存(例如,使用multiprocessing.Process
或concurrent.futures.ProcessPoolExecutor
)。
这通常会使事情变慢而不是更快。当临时存储器主要进入GUI时,显然不容易做到,因此必须存在于主进程中。
另一种选择是弄清楚内存的使用位置,而不是同时保留这么多对象。基本上,这有两个部分:
首先,在每个事件处理程序结束之前释放所有可能的内容。这意味着调用close
文件,del
对象或将对它们的所有引用设置为None
,在不可见的GUI对象上调用destroy
,并且所有,不存储你不需要的东西的引用。 (你真的需要在使用它之后保持PhotoImage
吗?如果你这样做,有没有办法按需加载图像?)
接下来,确保没有参考周期。在CPython中,只要没有循环就立即清理垃圾 - 但如果存在,它们会一直坐着,直到循环检查器运行。您可以使用gc
module对此进行调查。一个非常快速的事情是经常尝试这个:
print(gc.get_count())
gc.collect()
print(gc.get_count())
如果你看到巨大的跌落,你就有了周期。您必须查看gc.getobjects()
和gc.garbage
内部,或附加回调,或者仅仅查询您的代码以找到周期的确切位置。对于每一个,如果你真的不需要两个方向的参考,摆脱一个;如果您这样做,请将其中一个更改为weakref
。
答案 1 :(得分: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
除了哲学上的讨论之外,工业Edge计算的实际例子为我们提供了改善这一点的确切原因。如果在容器中运行Python,那么很快就会崩溃,特别是在生产负担沉重的情况下,在Edge上运行多个容器。
即使Edge具有16GiB,您也会很快遇到麻烦,尤其是使用Pandas等数据分析工具时。
然后,我的朋友,您将认识到垃圾回收器到底是什么,什么意思是“无法控制内存”。
C ++摇滚!!!