【问题标题】:Why doesn't tkinter release memory when an instance is destroyed?为什么实例被销毁时 tkinter 不释放内存?
【发布时间】:2018-10-16 15:30:15
【问题描述】:

我想要一种快速而肮脏的方法来获取一些文件名,而无需在我的 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() 的每个实例被销毁并删除 roots 之后,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() 不会释放占用的内存?

有什么明显的我错过了吗?我的测试不足以证实我的怀疑吗?我在这里和谷歌搜索过,但几乎没有找到答案。

编辑:以下是我尝试过(但失败了)的其他一些方法以及 cmets 中的建议:

# Force garbage collection
import gc
gc.collect()

# quit() method
root.quit()

# delete the entire tkinter reference
del tk

【问题讨论】:

  • 我认为垃圾收集器按照自己的方式运行。您可以在销毁 tk 实例后尝试强制 GC。看看有没有帮助。
  • 看看this post。 Alex Martelli 指出 python 使用了名为free list 的东西,这可能会导致您看到的内存问题。似乎有一种方法可以通过使用子流程来解决这个问题,所以如果这对你来说是个大问题,请调查一下。
  • 调用Tk() 所做的远远超过,而不仅仅是创建一个您可能不想要的窗口;它正在加载和初始化一个完全独立的编程环境,即实际实现所有 GUI 功能的 Tcl 解释器。对这个解释器保留一个隐式引用,以便Tkinter 模块中的函数可以真正完成它们的工作。
  • @Mike-SMT 这本身不是“问题”。诚然,Python 不会将内存返回给操作系统,但它可供将来使用,而不是向操作系统请求更多内存。
  • Tkinter 的“标准用法”是 GUI 在应用程序的整个生命周期中持续存在(事实上,应用程序),所以没有意义能够更早地摧毁它。 Tcl 解释器完全有可能甚至没有实现在进程终止之前彻底关闭自己的方法,因为这在本机 Tcl 应用程序中完全没有意义。

标签: python memory tkinter psutil


【解决方案1】:

这里有三个问题,一个是tkinter 的错,一个是你的错,一个是按预期行事。

三个问题分别是:

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

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

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

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

具体来说,看起来Tk 实例的.tk 属性没有被清理,即使您明确地destroy Tk 实例。您可以通过更改循环以摆脱对 Tk 对象的最后引用来限制内存增长,或者如果您只想释放低级 C 资源,则在 destroying 新 @ 之后显式取消链接 .tk 987654343@元素**:

# 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() 对象只是重复使用与第一个对象相同的内存(缺乏完全稳定性可能是由于堆碎片导致需要少量额外分配)。

【讨论】:

  • 部分额外内存将用于维护一个不断增长的对死 tkinter 实例的引用列表。如果您真的打算回收所有内存,为什么要在列表中保留引用?为什么不从列表中删除它们的引用?我认为这会触发对 .tk 引用等对象的任何额外垃圾收集。
  • 不错的努力。根据您的见解,我还尝试了root[-1].__dict__.clear() 来查看是否有任何其他对象占用了内存,而且似乎没有比简单的del root[-1].tk 有任何改进。虽然它并不能完全解决问题(并且在 20 次迭代中的第 8~15 次迭代中仍然会出现奇怪的内存跳跃),但它似乎确实确定了一个主要的罪魁祸首。如果没有其他答案,我很乐意接受这个答案。
  • 您似乎遇到了不必要的麻烦。为什么不直接打电话给root.pop()
  • @BryanOakley 可以在这个特定的测试中使用,但是如果我 rootTk() 的单个实例,我将不得不调用 del root.tk,不是吗?
  • @idlehands:我不完全确定你在问什么。当您删除root 时,root 的所有属性也将被删除。这里不需要手动删除root.tk,因为root 本身将不复存在。
【解决方案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()。如果您想要更通用的东西,请创建一个函数,在第一次调用它时创建根窗口并缓存该窗口,以便它只需要创建一次。

【讨论】:

    猜你喜欢
    • 2013-07-06
    • 1970-01-01
    • 1970-01-01
    • 2016-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-31
    • 2012-02-01
    相关资源
    最近更新 更多