实例销毁后,为什么tkinter不释放内存?

时间:2018-10-16 15:30:15

标签: 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()会释放占用的内存?

有什么明显的我想念的吗?我的测试不足以证实我的怀疑吗?我已经在这里和Google上进行了搜索,但没有找到答案。

编辑:以下是我尝试过(但失败了)的其他一些方法,其中包括注释中的建议:

# Force garbage collection
import gc
gc.collect()

# quit() method
root.quit()

# delete the entire tkinter reference
del tk

2 个答案:

答案 0 :(得分:2)

这里有三个问题,其中一个是tkinter的过错,其中一个是您的过错,其中一个是您的预期行为。

三个问题是:

  1. tkinter在注册其清理处理程序时会创建一个无法检测到的参考周期,只有通过显式调用destroy才能打破该参考周期(如果不这样做,则参考周期永远是 已清理,并且资源被永久保留)
  2. 即使您Tk个对象,也要坚持使用它们destroy
  3. 小对象堆很少(如果有的话)在程序终止之前返回操作系统(保留内存以备将来分配)

问题1意味着,如果有恢复内存的机会,您必须 destroy明确创建的任何Tk

问题2意味着,如果要使该内存可用于其他用途,则必须在创建新内存之前明确删除对Tk的任何引用(在destroy之后)。在某些情况下,您还希望显式设置tk.NoDefaultRoot()来防止创建的第一个Tk作为默认根被缓存在tkinter上(即,显式调用{{ 1}}在这样的对象上将清除缓存的默认根,因此在很多情况下这不会成为问题。

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

具体地说,即使您明确list .tk实例Tk实例的destroy属性也没有被清除>。您可以通过更改循环以摆脱对Tk对象的最后引用来限制内存的增长,或者如果您只想释放低级C资源,请在{{1 }}新增Tk元素**:

.tk

根据我经过稍微修改的脚本的输出,明确清除引用可以清除基础资源:

destroy

第一个对象从未完全回收内存的事实不足为奇。除非分配巨大(足够大以触发模式切换,该切换会向操作系统发出独立的请求,以独立于“小对象堆”)。否则,它们将维护一个空闲的内存列表,该列表不再使用并且可以重复使用。

这里大约6 MB的“浪费”可能是创建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() 对象只是在重用与第一个对象相同的内存(完全稳定的缺乏可能是由于堆碎片导致需要少量额外分配)。

答案 1 :(得分:2)

创建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>}

调用root.destroy()时,您只是销毁了小部件本身(实际上是_tclCommands列表中的元素)。该对象的其他部分保持不变。

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

请注意如何将_tclCommands设置为None,但是其余属性仍然占用内存。其中之一,tk占用了大量的内存,从未回收。

要完全删除对象,您需要删除它。在您的情况下,您需要从列表中删除该项目,以便不再有对该对象的引用。然后,您可以等待垃圾收集器正常工作,也可以显式调用垃圾收集器。

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


话虽如此,tkinter并非旨在以这种方式使用。基本的期望是您在程序开始时创建Tk的单个实例,并使该单个实例保持活动状态,直到程序退出。

对于您的情况,我建议您在程序启动时一次创建根窗口,然后将其隐藏。然后,您可以在整个程序中随意调用askopenfile()。如果您需要更多通用的功能,请创建一个函数,该函数在首次调用根窗口时创建该窗口并缓存该窗口,因此它只需创建一次即可。