Buscar..


Observaciones

En su núcleo, el recolector de basura de Python (a partir de 3.5) es una implementación de conteo de referencia simple. Cada vez que hace una referencia a un objeto (por ejemplo, a = myobject ) el recuento de referencia en ese objeto (myobject) se incrementa. Cada vez que se elimina una referencia, el recuento de referencias disminuye, y una vez que el recuento de referencias llega a 0 , sabemos que nada tiene una referencia a ese objeto y podemos desasignarlo.

Un malentendido común acerca de cómo funciona la administración de memoria de Python es que la palabra clave del delimita la memoria de los objetos. Esto no es verdad. Lo que sucede en realidad es que la palabra clave del Delte simplemente reduce los refcount de los objetos, lo que significa que si lo llama suficientes veces para que el refcount llegue a cero, el objeto puede ser recolectado como basura (incluso si en realidad todavía hay referencias al objeto disponible en otra parte de su código). ).

Python crea o limpia agresivamente los objetos la primera vez que los necesita. Si realizo la asignación a = object (), la memoria para el objeto se asigna en ese momento (cpython a veces reutilizará ciertos tipos de objetos, por ejemplo, listas bajo el capó, pero, en su mayoría, no mantiene un grupo de objetos libres y realizará la asignación cuando la necesite. De manera similar, tan pronto como el número de ref se reduce a 0, GC lo limpia.

Recolección de basura generacional

En la década de 1960, John McCarthy descubrió una falla fatal en el recuento de basura cuando implementó el algoritmo de recuento de cuentas utilizado por Lisp: ¿Qué sucede si dos objetos se refieren entre sí en una referencia cíclica? ¿Cómo puede alguna vez recolectar esos dos objetos de la basura incluso si no hay referencias externas a ellos si siempre se referirán entre ellos? Este problema también se extiende a cualquier estructura de datos cíclica, como los buffers de un anillo o dos entradas consecutivas en una lista con doble enlace. Python intenta solucionar este problema con un giro ligeramente interesante en otro algoritmo de recolección de basura llamado Generational Garbage Collection .

En esencia, cada vez que creas un objeto en Python, lo agrega al final de una lista doblemente enlazada. En ocasiones, Python recorre esta lista, comprueba a qué objetos se refieren los objetos de la lista, y si también están en la lista (veremos por qué podrían no estar en un momento), disminuye aún más sus refcounts. En este punto (en realidad, hay algunas heurísticas que determinan cuándo se mueven las cosas, pero supongamos que después de una sola colección para mantener las cosas simples) cualquier cosa que aún tenga un refcount mayor que 0 se promociona a otra lista vinculada llamada "Generación 1" (esta es la razón por la que no todos los objetos están siempre en la lista de la generación 0) que tiene este bucle aplicado con menos frecuencia. Aquí es donde entra en juego la recolección de basura generacional. Hay 3 generaciones de forma predeterminada en Python (tres listas vinculadas de objetos): La primera lista (generación 0) contiene todos los objetos nuevos; si ocurre un ciclo GC y los objetos no se recolectan, se mueven a la segunda lista (generación 1), y si ocurre un ciclo GC en la segunda lista y aún no se recolectan, se mueven a la tercera lista (generación 2 ). La lista de la tercera generación (llamada "generación 2", dado que no tenemos ninguna indexación) es recogida de basura con mucha menos frecuencia que las dos primeras, y la idea es que si su objeto tiene una larga vida útil, no es tan probable que se realice la GCed, y puede que nunca estar en GC durante la vida útil de su aplicación, por lo que no tiene sentido perder el tiempo en cada ejecución del GC. Además, se observa que la mayoría de los objetos se recolectan basura relativamente rápido. De ahora en adelante, llamaremos a estos "buenos objetos" ya que mueren jóvenes. Esto se denomina "hipótesis generacional débil" y también se observó por primera vez en los años 60.

Un lado rápido: a diferencia de las dos primeras generaciones, la lista de tercera generación de larga duración no es recogida de basura en un horario regular. Se verifica cuando la proporción de objetos pendientes de larga duración (aquellos que están en la lista de la tercera generación, pero que aún no han tenido un ciclo GC) con el total de objetos de larga duración en la lista es superior al 25%. Esto se debe a que la tercera lista no tiene límites (las cosas nunca se mueven de ella a otra lista, por lo que solo desaparecen cuando en realidad se recolectan basura), lo que significa que para las aplicaciones en las que está creando muchos objetos de larga duración, los ciclos de GC en la tercera lista puede llegar a ser bastante largo. Al utilizar una relación, logramos "rendimiento lineal amortizado en el número total de objetos"; también conocido, cuanto más larga sea la lista, más tiempo lleva GC, pero con menos frecuencia realizamos GC (aquí está la propuesta original de 2008 para esta heurística de Martin von Löwis para mayor lectura). El acto de realizar una recolección de basura en la tercera generación o lista "madura" se denomina "recolección de basura completa".

Así que la recolección de basura generacional acelera las cosas tremendamente al no requerir que escaneamos objetos que no es probable que necesiten GC todo el tiempo, pero ¿cómo nos ayuda a romper las referencias cíclicas? Probablemente no muy bien, resulta. La función para realmente romper estos ciclos de referencia comienza así :

/* Break reference cycles by clearing the containers involved.  This is
 * tricky business as the lists can be changing and we don't know which
 * objects may be freed.  It is possible I screwed something up here.
 */
static void
delete_garbage(PyGC_Head *collectable, PyGC_Head *old)

La razón por la que la recolección de basura generacional ayuda con esto es que podemos mantener la longitud de la lista como un conteo separado; cada vez que agregamos un nuevo objeto a la generación incrementamos este conteo, y cada vez que movemos un objeto a otra generación o lo tratamos, decrementamos el conteo. Teóricamente, al final de un ciclo de GC, este recuento (para las primeras dos generaciones de todos modos) siempre debe ser 0. Si no lo es, cualquier cosa que quede en la lista es alguna forma de referencia circular y podemos dejarla. Sin embargo, hay un problema más aquí: ¿Qué pasa si los objetos sobrantes tienen el método mágico de Python __del__ en ellos? __del__ se llama cada vez que se destruye un objeto de Python. Sin embargo, si dos objetos en una referencia circular tienen métodos __del__ , no podemos estar seguros de que destruir uno no romperá el método __del__ los otros. Para un ejemplo artificial, imagina que escribimos lo siguiente:

class A(object):
    def __init__(self, b=None):
        self.b = b
 
    def __del__(self):
        print("We're deleting an instance of A containing:", self.b)
     
class B(object):
    def __init__(self, a=None):
        self.a = a
 
    def __del__(self):
        print("We're deleting an instance of B containing:", self.a)

y establecemos una instancia de A y una instancia de B para que apunten entre sí y luego terminan en el mismo ciclo de recolección de basura? Digamos que elegimos uno al azar y desechamos nuestra instancia de A primero; Se __del__ método __del__ de A, se imprimirá y luego A se liberará. Luego llegamos a B, llamamos a su método __del__ , y ¡ __del__ ! Segfault! A ya no existe. Podríamos arreglar esto llamando primero a __del__ métodos __del__ que __del__ , luego haciendo otra pasada para repartir todo, sin embargo, esto introduce otro problema: ¿Qué pasa si uno de los __del__ método __del__ guarda una referencia del otro objeto que está a punto de ser GCed y ¿Tiene una referencia a nosotros en otro lugar? Todavía tenemos un ciclo de referencia, pero ahora no es posible hacer GC en ninguno de los dos objetos, incluso si ya no están en uso. Tenga en cuenta que incluso si un objeto no es parte de una estructura de datos circular, podría revivir en su propio método __del__ ; Python tiene una verificación de esto y detendrá la GCing si un refcount de objetos ha aumentado después de que se haya llamado a su método __del__ .

CPython se ocupa de esto al pegar esos objetos __del__ de GC (cualquier cosa con algún tipo de referencia circular y un método __del__ ) en una lista global de basura no recolectable y luego dejarla ahí por toda la eternidad:

/* list of uncollectable objects */
static PyObject *garbage = NULL;

Recuento de referencias

La gran mayoría de la administración de memoria de Python se maneja con conteo de referencias.

Cada vez que se hace referencia a un objeto (por ejemplo, asignado a una variable), su recuento de referencia aumenta automáticamente. Cuando se elimina la referencia (por ejemplo, la variable queda fuera del alcance), su recuento de referencia se reduce automáticamente.

Cuando el recuento de referencia llega a cero, el objeto se destruye inmediatamente y la memoria se libera inmediatamente. Por lo tanto, para la mayoría de los casos, el recolector de basura ni siquiera es necesario.

>>> import gc; gc.disable()  # disable garbage collector
>>> class Track:
        def __init__(self):
            print("Initialized")
        def __del__(self):
            print("Destructed")
>>> def foo():
        Track()
        # destructed immediately since no longer has any references
        print("---")
        t = Track()
        # variable is referenced, so it's not destructed yet
        print("---")
        # variable is destructed when function exits
>>> foo()
Initialized
Destructed
---
Initialized
---
Destructed

Para demostrar aún más el concepto de referencias:

>>> def bar():
        return Track()
>>> t = bar()
Initialized
>>> another_t = t  # assign another reference
>>> print("...")
...
>>> t = None          # not destructed yet - another_t still refers to it
>>> another_t = None  # final reference gone, object is destructed
Destructed

Recolector de basura para ciclos de referencia

La única vez que se necesita el recolector de basura es si tiene un ciclo de referencia . El ejemplo simple de un ciclo de referencia es uno en el que A se refiere a B y B se refiere a A, mientras que nada más se refiere a A o B. No se puede acceder a A ni a B desde cualquier lugar del programa, por lo que se pueden destruir de forma segura. sin embargo, sus recuentos de referencia son 1 y, por lo tanto, no se pueden liberar únicamente con el algoritmo de recuento de referencia.

>>> import gc; gc.disable()  # disable garbage collector
>>> class Track:
        def __init__(self):
            print("Initialized")
        def __del__(self):
            print("Destructed")
>>> A = Track()
Initialized
>>> B = Track()
Initialized
>>> A.other = B
>>> B.other = A
>>> del A; del B  # objects are not destructed due to reference cycle
>>> gc.collect()  # trigger collection
Destructed
Destructed
4

Un ciclo de referencia puede ser arbitrario largo. Si A apunta a B apunta a C apunta a ... apunta a Z que apunta a A, entonces no se recolectarán A a Z, hasta la fase de recolección de basura:

>>> objs = [Track() for _ in range(10)]
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
>>> for i in range(len(objs)-1):
...     objs[i].other = objs[i + 1]
...
>>> objs[-1].other = objs[0]  # complete the cycle
>>> del objs                  # no one can refer to objs now - still not destructed
>>> gc.collect()
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
20

Efectos del comando del

Eliminar un nombre de variable del ámbito usando del v , o eliminar un objeto de una colección usando del v[item] o del[i:j] , o eliminar un atributo usando del v.name , o cualquier otra forma de eliminar referencias a un objeto, no desencadena ninguna llamada de destructor ni ninguna memoria liberada en sí misma. Los objetos solo se destruyen cuando su cuenta de referencia llega a cero.

>>> import gc
>>> gc.disable()  # disable garbage collector
>>> class Track:
        def __init__(self):
            print("Initialized")
        def __del__(self):
            print("Destructed")
>>> def bar():
    return Track()
>>> t = bar()
Initialized
>>> another_t = t  # assign another reference
>>> print("...")
...
>>> del t          # not destructed yet - another_t still refers to it
>>> del another_t  # final reference gone, object is destructed
Destructed

Reutilización de objetos primitivos.

Una cosa interesante a tener en cuenta que puede ayudar a optimizar sus aplicaciones es que las primitivas también se vuelven a contar bajo el capó. Echemos un vistazo a los números; para todos los enteros entre -5 y 256, Python siempre reutiliza el mismo objeto:

>>> import sys
>>> sys.getrefcount(1)
797
>>> a = 1
>>> b = 1
>>> sys.getrefcount(1)
799

Tenga en cuenta que refcount aumenta, lo que significa que a y b referencia al mismo objeto subyacente cuando se refieren a la primitiva 1 . Sin embargo, para números más grandes, Python en realidad no reutiliza el objeto subyacente:

>>> a = 999999999
>>> sys.getrefcount(999999999)
3
>>> b = 999999999
>>> sys.getrefcount(999999999)
3

Debido a que el refcount de 999999999 no cambia cuando se asigna a a y b se puede inferir que se refieren a dos objetos subyacentes diferentes, aunque ambos se les asigna el mismo primitivo.

Viendo el refcount de un objeto

>>> import sys
>>> a = object()
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del b
>>> sys.getrefcount(a)
2

Forzar la desasignación de objetos.

Puede forzar la desasignación de objetos incluso si su refcount no es 0 en Python 2 y 3.

Ambas versiones utilizan el módulo ctypes para hacerlo.

ADVERTENCIA: haciendo esto dejará su entorno Python inestable y propenso a estrellarse sin un rastreo! El uso de este método también podría introducir problemas de seguridad (bastante improbable). Desasigne solo los objetos que está seguro de que nunca volverá a hacer referencia. Siempre.

Python 3.x 3.0
import ctypes
deallocated = 12345
ctypes.pythonapi._Py_Dealloc(ctypes.py_object(deallocated))
Python 2.x 2.3
import ctypes, sys
deallocated = 12345
(ctypes.c_char * sys.getsizeof(deallocated)).from_address(id(deallocated))[:4] = '\x00' * 4

Después de ejecutar, cualquier referencia al objeto ahora desasignado hará que Python produzca un comportamiento indefinido o se bloquee, sin un rastreo. Probablemente hubo una razón por la que el recolector de basura no eliminó ese objeto ...

Si desasigna None , aparece un mensaje especial: Fatal Python error: deallocating None antes de que se bloquee.

Gestionando la recogida de basura.

Hay dos enfoques para influir cuando se realiza una limpieza de memoria. Influyen en la frecuencia con la que se realiza el proceso automático y el otro está activando manualmente una limpieza.

El recolector de basura se puede manipular ajustando los umbrales de recolección que afectan la frecuencia a la que se ejecuta el recolector. Python utiliza un sistema de gestión de memoria basado en la generación. Los nuevos objetos se guardan en la generación más nueva - generación0 y con cada colección sobrevivida, los objetos se promueven a las generaciones anteriores. Después de llegar a la última generación - generación2 , ya no se promocionan.

Los umbrales se pueden cambiar usando el siguiente fragmento de código:

import gc
gc.set_threshold(1000, 100, 10) # Values are just for demonstration purpose

El primer argumento representa el umbral para recolectar generation0 . Cada vez que el número de asignaciones supera el número de desasignaciones por 1000, se llamará al recolector de basura.

Las generaciones anteriores no se limpian en cada ejecución para optimizar el proceso. El segundo y tercer argumento son opcionales y controlan la frecuencia con la que se limpian las generaciones anteriores. Si la generación0 se procesó 100 veces sin limpiar la generación1 , entonces se procesará la generación1 . De manera similar, los objetos en la generación 2 se procesarán solo cuando los de la generación 1 se hayan limpiado 10 veces sin tocar la generación 2 .

Una instancia en la que es beneficioso establecer manualmente los umbrales es cuando el programa asigna una gran cantidad de objetos pequeños sin desasignarlos, lo que hace que el recolector de basura se ejecute con demasiada frecuencia (cada una de las asignaciones de objetos de umbral de generación ). A pesar de que el colector es bastante rápido, cuando se ejecuta en una gran cantidad de objetos plantea un problema de rendimiento. De todos modos, no hay una estrategia única para todos los límites para elegir los umbrales y es confiable en cada caso de uso.

La activación manual de una colección se puede hacer como en el siguiente fragmento de código:

import gc
gc.collect()

La recolección de basura se activa automáticamente en función del número de asignaciones y desasignaciones, no en la memoria consumida o disponible. En consecuencia, cuando se trabaja con objetos grandes, la memoria puede agotarse antes de que se active la limpieza automática. Esto es un buen caso de uso para llamar manualmente al recolector de basura.

Aunque es posible, no es una práctica recomendada. Evitar las pérdidas de memoria es la mejor opción. De todos modos, en grandes proyectos, detectar la pérdida de memoria puede ser una tarea fácil y activar manualmente una recolección de basura se puede usar como una solución rápida hasta una depuración adicional.

Para los programas de larga duración, la recolección de basura puede activarse en una base de tiempo o evento. Un ejemplo para el primero es un servidor web que activa una colección después de un número fijo de solicitudes. Para más adelante, un servidor web que activa una recolección de basura cuando se recibe un cierto tipo de solicitud.

No espere a que la recolección de basura se limpie

El hecho de que la recolección de basura se limpie no significa que deba esperar a que se limpie el ciclo de recolección de basura.

En particular, no debe esperar a que la recolección de basura cierre los manejadores de archivos, las conexiones de base de datos y las conexiones de red abiertas.

por ejemplo:

En el siguiente código, asume que el archivo se cerrará en el siguiente ciclo de recolección de basura, si f fue la última referencia al archivo.

>>> f = open("test.txt")
>>> del f

Una forma más explícita de limpiar es llamar a f.close() . Puede hacerlo aún más elegante, es decir, utilizando la instrucción with , también conocida como administrador de contexto :

>>> with open("test.txt") as f:
...     pass
...     # do something with f
>>> #now the f object still exists, but it is closed

La instrucción with permite sangrar su código debajo del archivo abierto. Esto hace que sea explícito y más fácil ver cuánto tiempo se mantiene abierto un archivo. También siempre cierra un archivo, incluso si se produce una excepción en el bloque while .



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow