gc — Recolector de basura¶
Propósito: | Administra la memoria utilizada por los objetos de Python |
---|
gc
expone el mecanismo de administración de memoria subyacente de Python,
el recolector de basura automático. El módulo incluye funciones para controlar
cómo funciona el recolector y para examinar los objetos conocidos por el
sistema, ya sea pendientes de recolección o atascados en ciclos de referencia y
que no se pueden liberar.
Rastreo de referencias¶
Con gc
, las referencias entrantes y salientes entre objetos se pueden usar
para encontrar ciclos en estructuras de datos complejas. Si se sabe que una
estructura de datos tiene un ciclo, se puede usar un código personalizado para
examinar sus propiedades. Si el ciclo está en código desconocido, las
funciones get_referents()
y get_referrers()
se pueden usar para
construir herramientas genéricas de depuración.
Por ejemplo, get_referents()
muestra los objetos referidos por los
argumentos de entrada.
import gc
import pprint
class Graph:
def __init__(self, name):
self.name = name
self.next = None
def set_next(self, next):
print('Linking nodes {}.next = {}'.format(self, next))
self.next = next
def __repr__(self):
return '{}({})'.format(
self.__class__.__name__, self.name)
# Construct a graph cycle
one = Graph('one')
two = Graph('two')
three = Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)
print()
print('three refers to:')
for r in gc.get_referents(three):
pprint.pprint(r)
En este caso, la instancia Graph
three
contiene referencias a su
diccionario de instancias (en el atributo __dict__
) y su clase.
$ python3 gc_get_referents.py
Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)
three refers to:
{'name': 'three', 'next': Graph(one)}
<class '__main__.Graph'>
El siguiente ejemplo utiliza una Queue
para realizar un recorrido
transversal de todas las referencias de objetos en busca de ciclos. Los
elementos insertados en la cola son tuplas que contienen la cadena de
referencia hasta ahora y el siguiente objeto a examinar. Comienza con
three
y analiza todo a lo que se refiere. Omitir clases evita mirar
métodos, módulos, etc.
import gc
import pprint
import queue
class Graph:
def __init__(self, name):
self.name = name
self.next = None
def set_next(self, next):
print('Linking nodes {}.next = {}'.format(self, next))
self.next = next
def __repr__(self):
return '{}({})'.format(
self.__class__.__name__, self.name)
# Construct a graph cycle
one = Graph('one')
two = Graph('two')
three = Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)
print()
seen = set()
to_process = queue.Queue()
# Start with an empty object chain and Graph three.
to_process.put(([], three))
# Look for cycles, building the object chain for each object
# found in the queue so the full cycle can be printed at the
# end.
while not to_process.empty():
chain, next = to_process.get()
chain = chain[:]
chain.append(next)
print('Examining:', repr(next))
seen.add(id(next))
for r in gc.get_referents(next):
if isinstance(r, str) or isinstance(r, type):
# Ignore strings and classes
pass
elif id(r) in seen:
print()
print('Found a cycle to {}:'.format(r))
for i, link in enumerate(chain):
print(' {}: '.format(i), end=' ')
pprint.pprint(link)
else:
to_process.put((chain, r))
El ciclo en los nodos se encuentra fácilmente al observar los objetos que ya
han sido procesados. Para evitar contener referencias a esos objetos, sus
valores id()
se almacenan en caché en un conjunto. Los objetos del
diccionario que se encuentran en el ciclo son los valores __dict__
para las
instancias Graph
, y contienen sus atributos de instancia.
$ python3 gc_get_referents_cycles.py
Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)
Examining: Graph(three)
Examining: {'name': 'three', 'next': Graph(one)}
Examining: Graph(one)
Examining: {'name': 'one', 'next': Graph(two)}
Examining: Graph(two)
Examining: {'name': 'two', 'next': Graph(three)}
Found a cycle to Graph(three):
0: Graph(three)
1: {'name': 'three', 'next': Graph(one)}
2: Graph(one)
3: {'name': 'one', 'next': Graph(two)}
4: Graph(two)
5: {'name': 'two', 'next': Graph(three)}
Forzar la recolección de basura¶
Aunque el recolector de basura se ejecuta automáticamente cuando el intérprete
ejecuta un programa, se puede activar para que se ejecute en un momento
específico cuando hay muchos objetos para liberar o no hay mucho trabajo y el
recolector no afectará el rendimiento de la aplicación. Activa la recolección
usando collect()
.
import gc
import pprint
class Graph:
def __init__(self, name):
self.name = name
self.next = None
def set_next(self, next):
print('Linking nodes {}.next = {}'.format(self, next))
self.next = next
def __repr__(self):
return '{}({})'.format(
self.__class__.__name__, self.name)
# Construct a graph cycle
one = Graph('one')
two = Graph('two')
three = Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)
# Remove references to the graph nodes in this module's namespace
one = two = three = None
# Show the effect of garbage collection
for i in range(2):
print('\nCollecting {} ...'.format(i))
n = gc.collect()
print('Unreachable objects:', n)
print('Remaining Garbage:', end=' ')
pprint.pprint(gc.garbage)
En este ejemplo, el ciclo se borra tan pronto como la recolección se ejecute
por primera vez, ya que nada se refiere a los nodos Graph
excepto ellos
mismos. collect()
devuelve el número de objetos «inalcanzables» que
encontró. En este caso, el valor es 6
porque hay tres objetos con sus
diccionarios de atributos de instancia.
$ python3 gc_collect.py
Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)
Collecting 0 ...
Unreachable objects: 6
Remaining Garbage: []
Collecting 1 ...
Unreachable objects: 0
Remaining Garbage: []
Encontrar referencias a objetos que no se pueden recolectar¶
Buscar el objeto que contiene una referencia a otro objeto es un poco más
complicado que ver a qué hace referencia un objeto. Debido a que el código que
pregunta por la referencia debe contener una referencia, algunos de los
referentes deben ser ignorados. Este ejemplo crea un ciclo gráfico, luego
trabaja a través de las instancias Graph
y elimina la referencia en el nodo
«padre».
import gc
import pprint
class Graph:
def __init__(self, name):
self.name = name
self.next = None
def set_next(self, next):
print('Linking nodes {}.next = {}'.format(self, next))
self.next = next
def __repr__(self):
return '{}({})'.format(
self.__class__.__name__, self.name)
def __del__(self):
print('{}.__del__()'.format(self))
# Construct a graph cycle
one = Graph('one')
two = Graph('two')
three = Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)
# Collecting now keeps the objects as uncollectable,
# but not garbage.
print()
print('Collecting...')
n = gc.collect()
print('Unreachable objects:', n)
print('Remaining Garbage:', end=' ')
pprint.pprint(gc.garbage)
# Ignore references from local variables in this module, global
# variables, and from the garbage collector's bookkeeping.
REFERRERS_TO_IGNORE = [locals(), globals(), gc.garbage]
def find_referring_graphs(obj):
print('Looking for references to {!r}'.format(obj))
referrers = (r for r in gc.get_referrers(obj)
if r not in REFERRERS_TO_IGNORE)
for ref in referrers:
if isinstance(ref, Graph):
# A graph node
yield ref
elif isinstance(ref, dict):
# An instance or other namespace dictionary
for parent in find_referring_graphs(ref):
yield parent
# Look for objects that refer to the objects in the graph.
print()
print('Clearing referrers:')
for obj in [one, two, three]:
for ref in find_referring_graphs(obj):
print('Found referrer:', ref)
ref.set_next(None)
del ref # remove reference so the node can be deleted
del obj # remove reference so the node can be deleted
# Clear references held by gc.garbage
print()
print('Clearing gc.garbage:')
del gc.garbage[:]
# Everything should have been freed this time
print()
print('Collecting...')
n = gc.collect()
print('Unreachable objects:', n)
print('Remaining Garbage:', end=' ')
pprint.pprint(gc.garbage)
Este tipo de lógica es exagerada si se entienden los ciclos, pero para un ciclo
inexplicable en los datos usando get_referrers()
puede exponer la relación
inesperada.
$ python3 gc_get_referrers.py
Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)
Collecting...
Unreachable objects: 0
Remaining Garbage: []
Clearing referrers:
Looking for references to Graph(one)
Looking for references to {'name': 'three', 'next': Graph(one)}
Found referrer: Graph(three)
Linking nodes Graph(three).next = None
Looking for references to Graph(two)
Looking for references to {'name': 'one', 'next': Graph(two)}
Found referrer: Graph(one)
Linking nodes Graph(one).next = None
Looking for references to Graph(three)
Looking for references to {'name': 'two', 'next': Graph(three)}
Found referrer: Graph(two)
Linking nodes Graph(two).next = None
Clearing gc.garbage:
Collecting...
Unreachable objects: 0
Remaining Garbage: []
Graph(one).__del__()
Graph(two).__del__()
Graph(three).__del__()
Umbrales de recolección y generaciones¶
El recolector de basura mantiene tres listas de objetos que ve mientras se ejecuta, uno para cada «generación» rastreada por el recolector. A medida que se examinan los objetos en cada generación, se recogen o envejecen en generaciones posteriores hasta que finalmente alcanzan la etapa en la que se mantienen permanentemente.
Las rutinas del recolector pueden ajustarse para que ocurran a diferentes
frecuencias en función de la diferencia entre el número de asignaciones de
objetos y las desasignaciones entre ejecuciones. Cuando el número de
asignaciones menos el número de desasignaciones es mayor que el umbral para la
generación, se ejecuta el recolector de basura. Los umbrales actuales se
pueden examinar con get_threshold()
.
import gc
print(gc.get_threshold())
El valor de retorno es una tupla con el umbral para cada generación.
$ python3 gc_get_threshold.py
(700, 10, 10)
Los umbrales se pueden cambiar con set_threshold()
. Este programa de
ejemplo usa un argumento de línea de comando para establecer el umbral para la
generación 0
y luego asigna una serie de objetos.
import gc
import pprint
import sys
try:
threshold = int(sys.argv[1])
except (IndexError, ValueError, TypeError):
print('Missing or invalid threshold, using default')
threshold = 5
class MyObj:
def __init__(self, name):
self.name = name
print('Created', self.name)
gc.set_debug(gc.DEBUG_STATS)
gc.set_threshold(threshold, 1, 1)
print('Thresholds:', gc.get_threshold())
print('Clear the collector by forcing a run')
gc.collect()
print()
print('Creating objects')
objs = []
for i in range(10):
objs.append(MyObj(i))
print('Exiting')
# Turn off debugging
gc.set_debug(0)
Los diferentes valores de umbral introducen los barridos de recolección de basura en diferentes momentos, que se muestran aquí porque la depuración está habilitada.
$ python3 -u gc_threshold.py 5
Thresholds: (5, 1, 1)
Clear the collector by forcing a run
gc: collecting generation 2...
gc: objects in each generation: 505 2161 4858
gc: done, 0.0010s elapsed
Creating objects
gc: collecting generation 0...
gc: objects in each generation: 5 0 7323
gc: done, 0.0000s elapsed
Created 0
Created 1
gc: collecting generation 0...
gc: objects in each generation: 4 2 7323
gc: done, 0.0000s elapsed
Created 2
Created 3
Created 4
gc: collecting generation 1...
gc: objects in each generation: 6 3 7323
gc: done, 0.0000s elapsed
Created 5
Created 6
Created 7
gc: collecting generation 0...
gc: objects in each generation: 6 0 7329
gc: done, 0.0000s elapsed
Created 8
Created 9
Exiting
Un umbral más pequeño hace que los barridos se ejecuten con más frecuencia.
$ python3 -u gc_threshold.py 2
Thresholds: (2, 1, 1)
Clear the collector by forcing a run
gc: collecting generation 2...
gc: objects in each generation: 505 2161 4858
gc: done, 0.0010s elapsed
gc: collecting generation 0...
gc: objects in each generation: 2 0 7323
gc: done, 0.0000s elapsed
Creating objects
gc: collecting generation 0...
gc: objects in each generation: 5 0 7323
gc: done, 0.0000s elapsed
gc: collecting generation 1...
gc: objects in each generation: 3 3 7323
gc: done, 0.0000s elapsed
Created 0
Created 1
gc: collecting generation 0...
gc: objects in each generation: 4 0 7325
gc: done, 0.0000s elapsed
Created 2
gc: collecting generation 0...
gc: objects in each generation: 7 1 7325
gc: done, 0.0000s elapsed
Created 3
Created 4
gc: collecting generation 1...
gc: objects in each generation: 4 3 7325
gc: done, 0.0000s elapsed
Created 5
gc: collecting generation 0...
gc: objects in each generation: 7 0 7329
gc: done, 0.0000s elapsed
Created 6
Created 7
gc: collecting generation 0...
gc: objects in each generation: 4 2 7329
gc: done, 0.0000s elapsed
Created 8
gc: collecting generation 1...
gc: objects in each generation: 7 3 7329
gc: done, 0.0000s elapsed
Created 9
Exiting
Depuración¶
La depuración de pérdidas de memoria puede ser un desafío. gc
incluye
varias opciones para exponer el funcionamiento interno para facilitar el
trabajo. Las opciones son máscaras de bits destinadas a combinarse y pasarse a
set_debug()
para configurar el recolector de basura mientras se ejecuta el
programa. La información de depuración se imprime en sys.stderr
.
El indicador DEBUG_STATS
activa el informe de estadísticas, lo que hace que
el recolector de basura informe cuando se está ejecutando, la cantidad de
objetos rastreados para cada generación y la cantidad de tiempo que llevó
realizar el barrido.
import gc
gc.set_debug(gc.DEBUG_STATS)
gc.collect()
print('Exiting')
Este resultado de ejemplo muestra dos ejecuciones separadas del recolector porque se ejecuta una vez cuando se invoca explícitamente y una segunda vez cuando termina el intérprete.
$ python3 gc_debug_stats.py
gc: collecting generation 2...
gc: objects in each generation: 618 1413 4860
gc: done, 0.0009s elapsed
Exiting
gc: collecting generation 2...
gc: objects in each generation: 1 0 6746
gc: done, 0.0022s elapsed
gc: collecting generation 2...
gc: objects in each generation: 113 0 6570
gc: done, 2930 unreachable, 0 uncollectable, 0.0012s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 3189
gc: done, 151 unreachable, 0 uncollectable, 0.0003s elapsed
Habilitar DEBUG_COLLECTABLE
y DEBUG_UNCOLLECTABLE
hace que el
recolector informe si cada objeto que examina puede o no ser recolectado. Si
ver los objetos que no se pueden recolectar no es suficiente información para
comprender dónde se retienen los datos, habilita DEBUG_SAVEALL
para que
gc
conserve todos los objetos que encuentre sin ninguna referencia en la
lista garbage
.
import gc
flags = (gc.DEBUG_COLLECTABLE |
gc.DEBUG_UNCOLLECTABLE |
gc.DEBUG_SAVEALL
)
gc.set_debug(flags)
class Graph:
def __init__(self, name):
self.name = name
self.next = None
def set_next(self, next):
self.next = next
def __repr__(self):
return '{}({})'.format(
self.__class__.__name__, self.name)
class CleanupGraph(Graph):
def __del__(self):
print('{}.__del__()'.format(self))
# Construct a graph cycle
one = Graph('one')
two = Graph('two')
one.set_next(two)
two.set_next(one)
# Construct another node that stands on its own
three = CleanupGraph('three')
# Construct a graph cycle with a finalizer
four = CleanupGraph('four')
five = CleanupGraph('five')
four.set_next(five)
five.set_next(four)
# Remove references to the graph nodes in this module's namespace
one = two = three = four = five = None
# Force a sweep
print('Collecting')
gc.collect()
print('Done')
# Report on what was left
for o in gc.garbage:
if isinstance(o, Graph):
print('Retained: {} 0x{:x}'.format(o, id(o)))
# Reset the debug flags before exiting to avoid dumping a lot
# of extra information and making the example output more
# confusing.
gc.set_debug(0)
Esto permite que los objetos se examinen después de la recolección de basura, lo que es útil si, por ejemplo, el constructor no se puede cambiar para imprimir la identificación del objeto cuando se crea cada objeto.
$ python3 -u gc_debug_saveall.py
CleanupGraph(three).__del__()
Collecting
gc: collectable <Graph 0x101fe1f28>
gc: collectable <Graph 0x103d02048>
gc: collectable <dict 0x101c92678>
gc: collectable <dict 0x101c926c0>
gc: collectable <CleanupGraph 0x103d02160>
gc: collectable <CleanupGraph 0x103d02198>
gc: collectable <dict 0x101fe73f0>
gc: collectable <dict 0x101fe7360>
CleanupGraph(four).__del__()
CleanupGraph(five).__del__()
Done
Retained: Graph(one) 0x101fe1f28
Retained: Graph(two) 0x103d02048
Retained: CleanupGraph(four) 0x103d02160
Retained: CleanupGraph(five) 0x103d02198
Para simplificar, DEBUG_LEAK
se define como una combinación de todas las
otras opciones.
import gc
flags = gc.DEBUG_LEAK
gc.set_debug(flags)
class Graph:
def __init__(self, name):
self.name = name
self.next = None
def set_next(self, next):
self.next = next
def __repr__(self):
return '{}({})'.format(
self.__class__.__name__, self.name)
class CleanupGraph(Graph):
def __del__(self):
print('{}.__del__()'.format(self))
# Construct a graph cycle
one = Graph('one')
two = Graph('two')
one.set_next(two)
two.set_next(one)
# Construct another node that stands on its own
three = CleanupGraph('three')
# Construct a graph cycle with a finalizer
four = CleanupGraph('four')
five = CleanupGraph('five')
four.set_next(five)
five.set_next(four)
# Remove references to the graph nodes in this module's namespace
one = two = three = four = five = None
# Force a sweep
print('Collecting')
gc.collect()
print('Done')
# Report on what was left
for o in gc.garbage:
if isinstance(o, Graph):
print('Retained: {} 0x{:x}'.format(o, id(o)))
# Reset the debug flags before exiting to avoid dumping a lot
# of extra information and making the example output more
# confusing.
gc.set_debug(0)
Ten en cuenta que debido a que DEBUG_SAVEALL
es habilitado por
DEBUG_LEAK
, incluso los objetos sin referencia que normalmente se habrían
recolectado y eliminado se conservan.
$ python3 -u gc_debug_leak.py
CleanupGraph(three).__del__()
Collecting
gc: collectable <Graph 0x1044e1f28>
gc: collectable <Graph 0x1044eb048>
gc: collectable <dict 0x101c92678>
gc: collectable <dict 0x101c926c0>
gc: collectable <CleanupGraph 0x1044eb160>
gc: collectable <CleanupGraph 0x1044eb198>
gc: collectable <dict 0x1044e7360>
gc: collectable <dict 0x1044e72d0>
CleanupGraph(four).__del__()
CleanupGraph(five).__del__()
Done
Retained: Graph(one) 0x1044e1f28
Retained: Graph(two) 0x1044eb048
Retained: CleanupGraph(four) 0x1044eb160
Retained: CleanupGraph(five) 0x1044eb198
Ver también
- Documentación de la biblioteca estándar para gc
- Notas para portar Python 2 a 3 para gc
weakref
– El móduloweakref
proporciona una forma de crear referencias a objetos sin aumentar su recuento de referencias, por lo que aún pueden ser recolectados.- Supporting Cyclic Garbage Collection – Material de base de la documentación de la interfaz de programación C de Python.
- How does Python manage memory? – Un artículo sobre la gestión de la memoria Python por Fredrik Lundh.