python - 为什么 tkinter 在销毁实例时不释放内存?

标签 python memory tkinter psutil

我想要一种快速而肮脏的方式来获取一些文件名而无需在我的 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 的错,一个是你的错,一个是按预期运行。

这三个问题是:

  1. tkinter 创建一个不可检测的引用循环作为注册其清理处理程序的一部分,只有通过显式调用 destroy 才能打破(如果你不这样做,引用循环从不清理,资源永远保留)
  2. 即使在您销毁它们
  3. 之后,您仍保留着您的Tk 对象
  4. 在程序终止之前,小对象堆很少(如果有的话)返回给操作系统(内存保留以供将来分配)

问题 #1 意味着您必须销毁任何您显式创建的Tk,如果有任何恢复内存的机会的话。

问题 #2 意味着你必须在创建一个新的之前显式地删除对 Tk 的任何引用(在 destroy 之后)如果你想要内存可用于其他目的。在某些情况下,您还希望显式设置 tk.NoDefaultRoot() 以防止您创建的第一个 Tk 被缓存在 tkinter 上作为默认根(也就是说,对此类对象显式调用 destroy 将清除缓存的默认根,因此在许多情况下这不会成为问题)。

问题 #3 意味着您必须急切地摆脱引用,而不是等到程序结束才删除您的 root list;如果你等到最后删除它,是的,内存将返回到堆,但不会返回到操作系统,所以看起来你仍在使用所有内存。但这不是真正的问题;如果操作系统需要 RAM,则未使用的内存将被分页到磁盘(它通常在事件页面之前分页空闲页面),并保留它可以提高大多数代码的性能。

具体来说,看起来 Tk 实例的 .tk 属性没有被清除,即使您明确地destroy Tk 实例。您可以通过更改循环以摆脱对 Tk 对象的最后引用来限制内存增长,或者如果您只想释放低级 C 资源,请显式取消链接 .tkdestroy 新的 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/

相关文章:

python - 运行 Tf.Keras 模型时内存不足

python - Tkinter:如何在 Canvas 窗口中获取框架以扩展到 Canvas 的大小?

python - Tkinter 嵌入图形子流程

C Linux 从内存中读取/写入字(段、偏移量)

c - 用gets()读char*报 "Core Dumped"错误(C语言)

python - 在未先打开 ide 的情况下无法打开 .py 文件

python - 在列与特定值匹配的数据框中获取整数行索引

python - 为什么 tkinter 不能在 CentOs 7 上导入

python - 如何通知pandas DataFrame修改?

c++ - 获取dylib模块的SizeOfImage和EntryPoint