Gerenciamento de memória e GC
Nesta aula, exploramos o gerenciamento de memória em Python, incluindo o mecanismo de contagem de referências, o problema de ciclos de referências, o módulo gc para coleta de lixo e os vazamentos de memória mais comuns. Com exemplos práticos e exercícios, você aprenderá a diagnosticar e evitar problemas de memória em seus programas.
Python gerencia a memória automaticamente, mas entender como isso funciona é essencial para escrever programas eficientes e evitar vazamentos. Nesta aula, vamos mergulhar no mecanismo de contagem de referências, que é a base do gerenciamento de memória em CPython, e depois explorar o módulo gc que lida com ciclos de referências. Você também aprenderá sobre os vazamentos de memória mais comuns e como evitá-los.
Vamos começar com o conceito fundamental: cada objeto em Python tem uma contagem de referências que é incrementada quando uma nova referência é criada e decrementada quando uma referência é removida. Quando essa contagem chega a zero, o objeto é imediatamente destruído e a memória é liberada. Esse modelo é simples e eficiente, mas tem uma limitação: ciclos de referências podem impedir a liberação de memória. Para resolver isso, Python inclui um coletor de lixo cíclico, implementado no módulo gc.
Contagem de referências
Em CPython, cada objeto possui um campo ob_refcnt que armazena o número de referências ao objeto. Quando você faz uma atribuição, passa um objeto como argumento ou o adiciona a uma lista, a contagem é incrementada. Quando uma variável sai do escopo ou é sobrescrita, a contagem é decrementada. Quando chega a zero, o objeto é liberado imediatamente.
Vamos ver como isso funciona na prática. O módulo sys oferece a função getrefcount() que retorna a contagem atual de referências. Note que a própria chamada à função temporariamente incrementa a contagem, então o valor real é o retornado menos 1.
import sys
# Criando uma referência
x = []
print(sys.getrefcount(x)) # Saída: 2 (1 de x + 1 da chamada)
y = x
print(sys.getrefcount(x)) # Saída: 3 (x, y e a chamada)
del y
print(sys.getrefcount(x)) # Saída: 2 (x e a chamada)
del x
# Agora o objeto está sem referências e é destruído
Quando o objeto é destruído, o método __del__ (se definido) é chamado. Isso pode ser útil para liberar recursos externos, mas cuidado: se você criar um ciclo com objetos que possuem __del__, o coletor de lixo pode não conseguir coletá-los, causando vazamento.
Ciclos
Um ciclo de referências ocorre quando dois ou mais objetos se referenciam mutuamente, formando um ciclo. Por exemplo, uma lista que contém a si mesma ou dois objetos que se apontam. Nesses casos, a contagem de referências nunca chega a zero, mesmo que não haja mais nenhuma referência externa ao ciclo. Isso significa que a memória não é liberada.
# Exemplo de ciclo simples
class Node:
def __init__(self):
self.other = None
# Criando dois nós que se referenciam mutuamente
a = Node()
b = Node()
a.other = b
b.other = a
# Removendo as referências externas
del a
del b
# O ciclo permanece, e a memória não é liberada
Para resolver isso, Python usa um coletor de lixo cíclico que detecta e coleta esses ciclos. Ele funciona periodicamente ou pode ser invocado manualmente via gc.collect(). O coletor rastreia objetos que podem participar de ciclos (como containers) e usa um algoritmo de marcação e varredura para identificar ciclos inacessíveis.
gc module
O módulo gc fornece funções para controlar o coletor de lixo. Você pode usá-lo para desabilitar a coleta automática, forçar uma coleta, inspecionar objetos rastreados e ajustar o limiar de geração.
import gc
# Desabilitar a coleta automática (não recomendado em produção)
gc.disable()
# Forçar uma coleta manual
gc.collect()
# Ver o número de objetos rastreados por geração
print(gc.get_count())
# Obter estatísticas detalhadas
print(gc.get_stats())
O coletor de lixo do Python é geracional: objetos são divididos em três gerações (0, 1 e 2). Novos objetos vão para a geração 0. Se sobrevivem a uma coleta, são promovidos para a próxima geração. O coletor coleta a geração 0 com mais frequência que a 1, e a 1 mais que a 2. Isso otimiza o desempenho, pois a maioria dos objetos é temporária.
Você pode ajustar os limiares usando gc.set_threshold(). Por exemplo, gc.set_threshold(700, 10, 5) define que a geração 0 é coletada quando há 700 objetos, a geração 1 quando há 10 coletas da geração 0, e a geração 2 quando há 5 coletas da geração 1.
Vazamentos comuns
Vazamentos de memória em Python geralmente não são vazamentos no sentido tradicional (memória não é liberada), mas sim referências acidentais que mantêm objetos vivos. Alguns padrões comuns incluem:
- Variáveis globais e caches persistentes: manter referências em estruturas globais ou caches sem controle.
- Callbacks e closures: funções que capturam variáveis de escopo externo e são mantidas em listas de eventos, impedindo a liberação desses objetos.
- Objetos com
__del__em ciclos: ciclos de referências com objetos que possuem__del__não podem ser coletados automaticamente, pois o coletor não sabe a ordem de destruição.
Vamos ver um exemplo de vazamento por cache:
# Cache que nunca é limpo
cache = {}
def process_data(key, data):
# Processa dados e armazena no cache
cache[key] = data
# Uso repetido com chaves únicas
for i in range(100000):
process_data(i, {"data": i})
# cache agora tem 100000 entradas, consumindo muita memória
Para evitar vazamentos, você pode usar weakref para criar referências fracas, que não incrementam a contagem de referências. Isso é útil para caches que não devem impedir a coleta de objetos. O módulo weakref permite criar referências que não mantêm o objeto vivo.
import weakref
class Data:
pass
obj = Data()
ref = weakref.ref(obj)
print(ref() is obj) # True
del obj
print(ref() is None) # True (objeto foi coletado)
Boas práticas e observações finais
- Evite manter referências globais a objetos grandes ou temporários.
- Use
weakrefpara caches e estruturas que não devem impedir a coleta. - Evite definir
__del__em classes que podem participar de ciclos; prefira usarweakref.finalizepara limpeza. - Monitore o uso de memória da sua aplicação e identifique picos anormais.
- Em aplicações de longa duração, considere desabilitar a coleta automática e executar
gc.collect()em momentos controlados.
Referências
- Python Data Model - Referências
- Documentação oficial do módulo gc
- sys.getrefcount - Documentação Python
- Documentação do módulo weakref
- PyMOTW: gc - Garbage Collector
- Real Python: Python Memory Management
- C API: Reference Counting
Exercícios
- Explique como a contagem de referências funciona em Python e por que ela não é suficiente para gerenciar ciclos de referências.
- Escreva um programa que crie um ciclo de referências e use o módulo
gcpara detectar e coletar o ciclo. Mostre que a memória é liberada após a coleta. - Qual é a diferença entre referências fortes e fracas? Dê um exemplo de uso de
weakrefpara evitar vazamento de memória em um cache. - Explique o que são gerações no coletor de lixo e como podemos ajustar os limiares para otimizar a coleta.
- Descreva duas situações comuns de vazamento de memória em Python e como evitá-las.
import gc
class Node:
pass
# Criar ciclo
a = Node()
b = Node()
a.ref = b
b.ref = a
# Remover referências externas
del a, b
# Verificar que o ciclo existe
print("Objetos antes da coleta:", gc.collect()) # Força coleta e retorna número de objetos coletados
weakref.WeakValueDictionary para que os valores sejam coletados quando não houver mais referências fortes. Exemplo: import weakref
class Data: pass
cache = weakref.WeakValueDictionary()
def get_data(key):
if key in cache:
return cache[key]
obj = Data()
cache[key] = obj
return obj
gc.set_threshold(threshold0, threshold1, threshold2), controlando quando cada geração é coletada. Por exemplo, aumentar o threshold0 reduz a frequência de coleta, o que pode melhorar o desempenho se houver muitos objetos temporários.weakref ou implementar uma política de expiração. 2) Callbacks que capturam variáveis de escopo externo e são mantidos em listas de eventos: remover o callback quando não for mais necessário, ou usar referências fracas. Além disso, evitar __del__ em ciclos, pois impede a coleta.