La memoria se pierde cuando la imagen se desecha en Python

Actualmente estoy escribiendo un juego de mesa simple en Python y me di cuenta de que la recolección de basura no borra los datos de bitmap descartados de la memoria cuando se recargan las imágenes. Sucede solo cuando el juego se inicia o se carga o la resolución cambia, pero multiplica la memoria consumida, por lo que no puedo dejar que este problema quede sin resolver.

Cuando se vuelven a cargar las imágenes, todas las referencias se transfieren a los nuevos datos de imagen, ya que están vinculados a la misma variable a la que se enlazaron los datos de la imagen original. Intenté forzar la recolección de basura utilizando collect() pero no sirvió de nada.

Escribí una pequeña muestra para demostrar mi problema.

 from tkinter import Button, DISABLED, Frame, Label, NORMAL, Tk from PIL.Image import open from PIL.ImageTk import PhotoImage class App(Tk): def __init__(self): Tk.__init__(self) self.text = Label(self, text = "Please check the memory usage. Then push button #1.") self.text.pack() self.btn = Button(text = "#1", command = lambda : self.buttonPushed(1)) self.btn.pack() def buttonPushed(self, n): "Cycle to open the Tab module n times." self.btn.configure(state = DISABLED) # disable to prevent paralell cycles if n == 100: self.text.configure(text = "Overwriting the bitmap with itself 100 times...\n\nCheck the memory usage!\n\nUI may seem to hang but it will finish soon.") self.update_idletasks() for i in range(n): # creates the Tab frame whith the img, destroys it, then recreates them to overwrite the previous Frame and prevous img b = Tab(self) b.destroy() if n == 100: print(i+1,"percent of processing finished.") if n == 1: self.text.configure(text = "Please check the memory usage now.\nMost of the difference is caused by the bitmap opened.\nNow push button #100.") self.btn.configure(text = "#100", command = lambda : self.buttonPushed(100)) self.btn.configure(state = NORMAL) # starting cycles is enabled again class Tab(Frame): """Creates a frame with a picture in it.""" def __init__(self, master): Frame.__init__(self, master = master) self.a = PhotoImage(open("map.png")) # img opened, change this to a valid one to test it self.b = Label(self, image = self.a) self.b.pack() # Label with img appears in Frame self.pack() # Frame appears if __name__ == '__main__': a = App() 

Para ejecutar el código anterior, necesitará un archivo de imagen PNG. Las dimensiones de mi map.png son 1062 × 1062. Como PNG es de 1.51 MB y como datos de bitmap es de aproximadamente 3-3.5 MB. Utilice una imagen grande para ver la pérdida de memoria fácilmente.

Resultado esperado cuando ejecuta mi código: el proceso de python consume la memoria ciclo por ciclo. Cuando consume aproximadamente 500 MB, se colapsa pero comienza a devorar la memoria.

Por favor, dame algunos consejos sobre cómo resolver este problema. Estoy agradecido por cada ayuda. Gracias. por adelantado.

Primero, definitivamente no tienes una pérdida de memoria. Si se “colapsa” cada vez que se acerca a los 500 MB y nunca lo cruza, no puede haber fugas.


Y mi conjetura es que no tienes ningún problema en absoluto.

Cuando el recolector de basura de Python limpia las cosas (lo que generalmente ocurre inmediatamente cuando terminas con CPython), generalmente no libera la memoria al sistema operativo. En su lugar, lo mantiene alrededor en caso de que lo necesite más adelante. Esto es intencional, a menos que se esté intercambiando, es mucho más rápido reutilizar la memoria que seguir liberándola y reasignándola.

Además, si 500 MB es una memoria virtual, eso no es nada en una plataforma moderna de 64 bits. Si no se asigna a la memoria física / residente (o se asigna si la computadora está inactiva, pero si no se lanza rápidamente), no es un problema; es solo que el sistema operativo es agradable con recursos que son efectivamente gratuitos.

Más importante aún: ¿Qué te hace pensar que hay un problema? ¿Hay algún síntoma real, o simplemente algo en el Administrador de progtwigs / Monitor de actividad / arriba / lo que sea que te asuste? (Si es lo último, eche un vistazo a los otros progtwigs. En mi Mac, tengo 28 progtwigs que se ejecutan actualmente con más de 400 MB de memoria virtual, y estoy usando 11 de 16 GB, aunque menos de 3 GB es en realidad cableado. Si, por ejemplo, arranco Logic, la memoria se recostackrá más rápido de lo que Logic puede usarla; hasta entonces, ¿por qué debería el sistema operativo desperdiciar el esfuerzo desasignando memoria (especialmente cuando no tiene forma de asegurarse de que algunos procesos no Ve y pregunta por ese recuerdo que no estaba usando mas tarde.


Pero si hay un problema real, hay dos maneras de resolverlo.


El primer truco es hacer todo el uso intensivo de memoria en un proceso secundario que puede matar y reiniciar para recuperar la memoria temporal (por ejemplo, mediante el uso de multiprocessing.Process o concurrent.futures.ProcessPoolExecutor ).

Esto generalmente hace que las cosas sean más lentas en lugar de más rápidas. Y, obviamente, no es fácil de hacer cuando la memoria temporal es principalmente cosas que van directamente a la GUI y, por lo tanto, tienen que vivir en el proceso principal.


La otra opción es averiguar dónde se está utilizando la memoria y no mantener tantos objetos al mismo tiempo. Básicamente, hay dos partes para esto:

Primero, libere todo lo posible antes del final de cada controlador de eventos. Esto significa llamar al close de archivos, ya sea eliminando objetos o configurando todas las referencias a ellos como None , llamando a destroy en objetos de la GUI que no están visibles y, sobre todo, no almacenar referencias a cosas que no necesita. (¿Realmente necesita mantener la PhotoImage después de usarla? Si lo hace, ¿hay alguna forma de cargar las imágenes a pedido?)

A continuación, asegúrese de que no tiene ciclos de referencia. En CPython, la basura se limpia inmediatamente siempre que no haya ciclos, pero si los hay, se quedan sentados hasta que se ejecuta el verificador de ciclos. Puedes usar el módulo gc para investigar esto. Una cosa realmente rápida es intentar esto de vez en cuando:

 print(gc.get_count()) gc.collect() print(gc.get_count()) 

Si ves grandes gotas, tienes ciclos. Tendrá que buscar en gc.getobjects() y gc.garbage , o adjuntar devoluciones de llamada, o simplemente razonar sobre su código para encontrar exactamente dónde están los ciclos. Para cada una, si realmente no necesita referencias en ambas direcciones, deshágase de una; Si lo haces, cambia uno de ellos en una weakref .