我想要一种快速而肮脏的方式来获取一些文件名而无需在我的 shell 中键入,所以我有以下这段代码:
from tkinter.filedialog import askopenfile
file = askopenfile()
现在一切正常,但它确实创建了一个需要关闭的多余 tkinter
GUI。我知道我可以这样做来抑制它:
import tkinter as tk
tk.Tk().withdraw()
但不代表后面没有装。这只是意味着现在有一个我无法关闭/销毁的 Tk()
对象。
这让我想到了真正的问题。
似乎每次我创建一个Tk()
,无论我是del
还是destroy()
,内存都不是释放。见下文:
import tkinter as tk
import os, psutil
process = psutil.Process(os.getpid())
def mem(): print(f'{process.memory_info().rss:,}')
# initial memory usage
mem()
# 21,475,328
for i in range(20):
root.append(tk.Tk())
root[-1].destroy()
mem()
# 24,952,832
# 26,251,264
# ...
# 47,591,424
# 48,865,280
# try deleting the root instead
del root
mem()
# 50,819,072
如上所示,即使在 Tk()
的每个实例都被销毁并且 root
被删除后,python 也不会释放使用量。然而,其他对象并非如此:
class Foo():
def __init__(self):
# create a list that takes up approximately the same size as a Tk() on average
self.lst = list(range(11500))
for i in range(20):
root.append(Foo())
del root[-1]
mem()
# 52,162,560
# 52,162,560
# ...
# 52,162,560
所以我的问题是,为什么 Tk()
和我的 Foo()
不同,为什么不破坏/删除 Tk( )
创建释放占用的内存?
我错过了什么明显的东西吗?我的测试是否不足以证实我的怀疑?我在这里和谷歌上进行了搜索,但几乎找不到答案。
编辑:以下是我根据评论中的建议尝试(但失败)的其他一些方法:
# Force garbage collection
import gc
gc.collect()
# quit() method
root.quit()
# delete the entire tkinter reference
del tk
最佳答案
这里有三个问题,一个是 tkinter
的错,一个是你的错,一个是按预期运行。
这三个问题是:
tkinter
创建一个不可检测的引用循环作为注册其清理处理程序的一部分,只有通过显式调用destroy
才能打破(如果你不这样做,引用循环从不清理,资源永远保留)- 即使在您
销毁
它们 之后,您仍保留着您的 - 在程序终止之前,小对象堆很少(如果有的话)返回给操作系统(内存保留以供将来分配)
Tk
对象
问题 #1 意味着您必须销毁
任何您显式创建的Tk
,如果有任何恢复内存的机会的话。
问题 #2 意味着你必须在创建一个新的之前显式地删除对 Tk
的任何引用(在 destroy
之后)如果你想要内存可用于其他目的。在某些情况下,您还希望显式设置 tk.NoDefaultRoot()
以防止您创建的第一个 Tk
被缓存在 tkinter
上作为默认根(也就是说,对此类对象显式调用 destroy
将清除缓存的默认根,因此在许多情况下这不会成为问题)。
问题 #3 意味着您必须急切地摆脱引用,而不是等到程序结束才删除您的 root
list
;如果你等到最后删除它,是的,内存将返回到堆,但不会返回到操作系统,所以看起来你仍在使用所有内存。但这不是真正的问题;如果操作系统需要 RAM,则未使用的内存将被分页到磁盘(它通常在事件页面之前分页空闲页面),并保留它可以提高大多数代码的性能。
具体来说,看起来 Tk
实例的 .tk
属性没有被清除,即使您明确地destroy
Tk
实例。您可以通过更改循环以摆脱对 Tk
对象的最后引用来限制内存增长,或者如果您只想释放低级 C 资源,请显式取消链接 .tk
在 destroy
新的 Tk
元素之后**:
# Not necessary, but avoids caching any Tk as a root when you don't want it
tk.NoDefaultRoot()
root = [] # Missing in your original code, but I'm assuming it was a plain list
for i in range(20):
root.append(tk.Tk())
root[-1].destroy()
# Either drop the reference to the `Tk` completely:
root[-1] = None
# or just drop the reference to its C level worker object
root[-1].tk = None
# Optionally, call gc.collect() here to forcibly reclaim memory faster
# otherwise you're likely to see memory usage grow by a few KB as uncleaned
# cycles aren't reclaimed in time so we see phantom leaks (that would
# eventually be cleaned)
mem()
根据我稍微修改过的脚本的输出,显式清除引用允许清除底层资源:
12,152,832
17,539,072
17,924,096 # At this point, the original code was above 18.8M bytes
17,965,056
17,965,056 # At this point, the original code was above 21.7M bytes
... remains unchanged until end of program if gc.collect() called regularly ...
第一个对象的内存永远不会完全回收这一事实不足为奇。内存分配器很少费心实际将内存返回给操作系统,除非分配巨大(大到足以触发模式切换,该模式切换向操作系统独立请求与“小对象堆”)。否则,他们维护一个不再使用且可以重复使用的内存空闲列表。
这里大约 6 MB 的“浪费”可能是创建 Tk
对象本身和它管理的对象树时涉及的一堆小分配,虽然随后返回到堆中重用,在程序退出之前不会返回给操作系统(也就是说,如果堆的那部分不再使用,如果内存不足,操作系统可能会优先将未使用的部分分页到磁盘)。通过注意到内存使用几乎立即稳定下来,您可以看到这种优化有何帮助;新的 tk.Tk()
对象只是重复使用与第一个相同的内存(缺乏完全稳定性可能是由于堆碎片导致需要少量额外分配)。
关于python - 为什么 tkinter 在销毁实例时不释放内存?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52839050/