There are three issues here, one of which is tkinter
's fault, one of which is yours, and one of which is behaving as intended.
The three issues are:
tkinter
creates an undetectable reference cycle as part of registering its cleanup handlers, which is only broken by explicitly calling destroy
(if you don't do so, the reference cycle is never cleaned, and the resources are held forever)
- You're holding on to your
Tk
objects even after you destroy
them
- The small object heap is rarely, if ever, returned to the OS before program termination (the memory is kept around for future allocations)
Problem #1 means you must destroy
any Tk
you create explicitly if there is any chance of recovering the memory.
Problem #2 means that you must explicitly get rid of any reference to a Tk
(after destroy
ing it) before creating a new one if you want the memory to be available for other purposes. In some cases, you'd also want to explicitly set tk.NoDefaultRoot()
to prevent the first Tk
you create from being cached on tkinter
as the default root (that said, explicit calls to destroy
on such an object will clear the cached default root, so this isn't going to be a problem in many cases).
Issue #3 means you must get rid of the references eagerly, rather than waiting until the end of the program to delete your root
list
; if you wait until the end to delete it, yes, the memory will be returned to the heap, but not to the OS, so it will look like you're still using all of it. It's not a real problem though; the unused memory will be paged out to disk if the OS is in need of RAM (it usually pages idle pages before active ones), and keeping it around improves the performance of most code.
Specifically, it looks like the .tk
attribute of Tk
instances isn't being cleaned up even when you explicitly destroy
the Tk
instance. You can cap the memory growth by changing your loop to get rid of the last reference to the Tk
object, or if you just want to free the low level C resources, explicitly unlink .tk
after destroy
ing the new Tk
element**:
# 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()
Explicitly clearing the reference allows the underlying resources to be cleaned, based on the output from my slightly modified script:
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 ...
The fact that the memory is never completely reclaimed for the first object isn't surprising. Memory allocators rarely bother to actually return the memory to the operating system unless the allocation was huge (large enough to trigger a mode switch that makes an independent request to the OS for memory that is managed separately from the "small object heap"). Otherwise, they maintain a free list of memory that is no longer in use and can be reused.
The ~6 MB of "waste" here was likely a bunch of small allocations involved in creating the Tk
object itself and the tree of objects it manages, that, while subsequently returned to the heap for reuse, will not be returned to the OS until the program exits (that said, if that part of the heap is never used again, the OS may preferentially page the unused parts out to disk if it runs low on memory). You can see how this optimization helped by noticing that the memory use stabilizes almost immediately; the new tk.Tk()
objects are just reusing the same memory as the first ones (the lack of complete stability is likely due heap fragmentation causing a need for small additional allocations).
tkinter
itself could be holding references to the objects. – Vudimirgc.collect()
ordel tk
did nothing to reduce the memory footprint. – Cholerroot[-1].destroy()
is not the same asdel root[-1]
. (Although I would expect the garbage collector to clean everything up soon after you rundel root
.) – Vudimirfree list
and this can cause the memory issue you are seeing. There appears to be a way to work around this by using a subprocess so look into that if it is a big issue for you. – JeffreyjeffreysTk()
does far, far more than just creating a window you might not want; it's loading and initializing an entirely separate programming environment, the Tcl interpreter that actually implements all of the GUI functionality. An implicit reference is kept to this interpreter, so that the functions in theTkinter
module can actually do their work. – Bossismfree list
in python. – Jeffreyjeffreysroot.destroy()
would have also cleaned up the external interpreter if that's the case. At the very least, I wouldn't have expected multiple interpreter would be created each time I callTk()
, given that the memory increment is loosely consistent between eachTk()
call. I'm not trying to rag ontkinter
, but not having a way to implicitly/explicitly murder the interpreter upon finishing the GUI feels disappointing. – Cholerdestroy()
gets rid of the object andquit()
is suppose to end the interpreter for tk. That said I also testedquit()
with the same results on memory. – Jeffreyjeffreysquit()
doesn't show a memory decrease as well. – Cholersubprocess
wouldn't be pretty either. I just find it odd that a standard module of this magnitude doesn't have a way to optimize the memory implicitly or explicitly, and believed I must be missing something. – Cholerfree list
. I am sure its benefit outweighs the memory cost. – JeffreyjeffreysTk()
s first, which incremented to about 50MB usage after trying to delete/destroy everything. Subsequently when I tried myFoo()
run, the memory usage just increased over 50MB instead of using the presumed "unreturned" memory. Python was requesting more mem from OS in lieu of the mem taken up byTk()
s. – Choleraskopenfile()
the memory increases and doesn't go back down, hence this investigation. You're right for most common usage though. – Choler