当实例被销毁时,为什么tkinter不释放内存?

r.o*_*ook 5 python memory tkinter psutil

我想要一个快速而又脏的方法来获取一些文件名而无需在我的shell中输入,所以我有以下代码:

from tkinter.filedialog import askopenfile

file = askopenfile()
Run Code Online (Sandbox Code Playgroud)

现在,这一切工作正常,但它确实创造一个多余的tkinter,需要关闭GUI.我知道我可以这样做来压制它:

import tkinter as tk
tk.Tk().withdraw()    
Run Code Online (Sandbox Code Playgroud)

但这并不意味着它没有装在背上.它只是意味着现在有一个Tk()我无法关闭/销毁的对象.


所以这让我想到了真正的问题.

似乎每次我创建一个Tk(),无论我deldestroy()它,内存都没有被释放.见下文:

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
Run Code Online (Sandbox Code Playgroud)

正如所看到的,蟒蛇甚至没有的每个实例后腾出的使用Tk()被破坏和root小号删除.然而,这不是其他对象的情况:

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
Run Code Online (Sandbox Code Playgroud)

所以我的问题是,为什么它Tk()与我之间有所区别Foo(),为什么不破坏/删除所Tk()创建的释放内存?

我错过了一些明显的东西吗?我的测试不足以证实我的怀疑吗?我在这里和谷歌搜索但发现答案很少.

编辑:以下是我尝试(并失败)的一些其他方法,其中包含评论中的建议:

# Force garbage collection
import gc
gc.collect()

# quit() method
root.quit()

# delete the entire tkinter reference
del tk
Run Code Online (Sandbox Code Playgroud)

Sha*_*ger 7

这里存在三个问题,其中之一是tkinter你的错误,其中之一是你的错误,还有一个是你的行为不符合预期。

这三个问题是:

  1. tkinter创建一个不可检测的引用循环作为注册其清理处理程序的一部分,只有通过显式调用才能破坏该destroy引用循环(如果您不这样做,则引用循环永远不会被清理,并且资源将永远保留)
  2. Tk即使你已经拿到了你的物品,你仍然会抓住destroy它们
  3. 在程序终止之前,小对象堆很少(如果有的话)返回到操作系统(内存保留以供将来分配)

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

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

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

具体来说.tkTkdestroyTk,即使您显式指定实例,实例属性也不会被清除。您可以通过更改循环以消除对对象的最后一个引用来限制内存增长Tk,或者如果您只想释放低级 C 资源,请在新元素.tk之后显式取消链接**:destroyTk

# 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()
Run Code Online (Sandbox Code Playgroud)

根据我稍微修改的脚本的输出,显式清除引用可以清除底层资源:

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 ...
Run Code Online (Sandbox Code Playgroud)

事实上,第一个对象的内存永远不会被完全回收,这一事实并不奇怪。内存分配器很少会费心将内存实际返回给操作系统,除非分配量很大(大到足以触发模式切换,向操作系统发出与“小对象堆”分开管理的内存的独立请求)。否则,它们会维护一个不再使用且可以重用的空闲内存列表。

这里大约 6 MB 的“浪费”可能是在创建对象本身及其管理的对象树时涉及的一堆小分配Tk,虽然随后返回到堆以供重用,但不会返回到操作系统,直到程序退出(也就是说,如果堆的该部分不再使用,操作系统可能会优先将未使用的部分分页到磁盘(如果内存不足))。您可以通过注意到内存使用几乎立即稳定下来来了解此优化的帮助;新tk.Tk()对象只是重用与第一个对象相同的内存(缺乏完全稳定性可能是由于堆碎片导致需要少量额外分配)。


Bry*_*ley 4

当您创建 的实例时Tk,您创建的不仅仅是一个小部件。您正在创建一个具有多个属性(嵌入式 tcl 解释器、小部件列表等)的对象。当您这样做时root.destroy(),您只是破坏了该对象拥有的部分数据。对象本身仍然存在并占用内存。由于您在列表中保留对该对象的引用,因此该对象永远不会被垃圾收集,因此内存会挂起。

当您使用 创建根窗口时root = tk.Tk(),您将得到一个对象 ( root)。如果您使用 vars 查看该对象的属性,您会看到以下内容:

>>> root = tk.Tk()
>>> vars(root)
{'children': {}, '_tkloaded': 1, 'master': None, '_tclCommands': ['tkerror', 'exit', '4463962184destroy'], 'tk': <_tkinter.tkapp object at 0x10a1d7f30>}
Run Code Online (Sandbox Code Playgroud)

当您调用 时root.destroy(),您只是销毁小部件本身(本质上是列表中的元素_tclCommands)。该物体的其他部分保持完好。

>>> root.destroy()
>>> vars(root)
{'children': {}, '_tkloaded': 1, 'master': None, '_tclCommands': None, 'tk': <_tkinter.tkapp object at 0x10a1d7f30>}
Run Code Online (Sandbox Code Playgroud)

请注意如何_tclCommands设置为None,但其余属性仍然占用内存。其中之一tk占用了大量永远不会被回收的内存。

要完全删除该对象,您需要将其删除。在您的情况下,您需要从列表中删除该项目,以便不再有对该对象的任何引用。然后,您可以等待垃圾收集器发挥其魔力,或者您可以显式调用垃圾收集器。

这可能不会回收 100% 的内存,但它应该可以让您非常接近。


话虽这么说,tkinter 并不是为了以这种方式使用而设计的。Tk基本期望是您在程序开始时创建一个实例,并保持该实例处于活动状态直到程序退出。

对于您的情况,我建议您在程序启动时创建根窗口一次,然后将其隐藏。然后,您可以askopenfile()在整个计划期间随意拨打电话。如果您想要更通用的功能,请创建一个函数,该函数在第一次调用时创建根窗口并缓存该窗口,以便只需创建一次。