Tudo o que você precisa saber sobre o coletor de lixo em Python

Geralmente, você não precisa se preocupar com o coletor de lixo e trabalhar com memória ao escrever código Python. Assim que os objetos não são mais necessários, o Python libera automaticamente a memória deles. Apesar disso, entender como o GC funciona ajudará você a escrever um código melhor.

Gerenciador de memória


Diferentemente de outras linguagens populares, o Python não libera toda a memória de volta para o sistema operacional assim que ele exclui um objeto. Em vez disso, ele usa um gerenciador de memória adicional projetado para objetos pequenos (cujo tamanho é menor que 512 bytes). Para trabalhar com esses objetos, ele aloca grandes blocos de memória, nos quais muitos objetos pequenos serão armazenados no futuro.

Assim que um dos objetos pequenos é excluído - a memória de baixo dele não vai para o sistema operacional, o Python o deixa para novos objetos do mesmo tamanho. Se não houver objetos restantes em um dos blocos de memória alocados, o Python poderá liberá-lo para o sistema operacional. Normalmente, os blocos são liberados quando o script cria muitos objetos temporários.

Portanto, se um processo Python de longa duração começar a consumir mais memória ao longo do tempo, isso não significa que o seu código tenha um problema de vazamento de memória. Se você quiser saber mais sobre o gerenciador de memória no Python, leia sobre isso no meu outro artigo .

Algoritmos de Coleta de Lixo


O interpretador python padrão (CPython) usa dois algoritmos ao mesmo tempo, contagem de referência e coletor de lixo geracional (doravante GC), mais conhecido como módulo gc padrão do Python.

O algoritmo de contagem de links é muito simples e eficiente, mas tem uma grande desvantagem. Ele não sabe como definir referências circulares. É por isso que em python há um coletor adicional chamado GC geracional que monitora objetos com possíveis referências circulares.

No Python, o algoritmo de contagem de referência é fundamental e não pode ser desativado, enquanto o GC é opcional e pode ser desativado.

Algoritmo de contagem de links


O algoritmo de contagem de links é uma das técnicas mais fáceis de coleta de lixo. Os objetos são excluídos assim que não são mais referenciados.

No Python, as variáveis ​​não armazenam valores, mas atuam como referências a objetos. Ou seja, quando você atribui um valor a uma nova variável, primeiro um objeto com esse valor é criado e somente então a variável começa a se referir a ele. Várias variáveis ​​podem fazer referência a um único objeto.

Cada objeto no Python contém um campo adicional (contador de referência), que armazena o número de links para ele. Assim que alguém se refere a um objeto, esse campo é incrementado em um. Se, por algum motivo, o link desaparecer, esse campo será reduzido em um.

Exemplos quando o número de links aumenta:

  • operador de atribuição
  • passando argumentos
  • insira um novo objeto na planilha (o número de links para o objeto aumenta)
  • uma construção do formulário foo = bar (foo começa a se referir ao mesmo objeto que bar)

Assim que o contador de referência para um objeto específico atingir zero, o intérprete inicia o processo de destruição do objeto. Se o objeto remoto contiver links para outros objetos, esses links também serão excluídos. Assim, a remoção de um objeto pode implicar a remoção de outros.

Por exemplo, se uma lista for excluída, a contagem de referência em todos os seus elementos será reduzida em um. Se todos os objetos da lista não forem usados ​​em nenhum outro lugar, eles também serão excluídos.

Variáveis ​​declaradas fora de funções, classes e blocos são chamadas globais. Normalmente, o ciclo de vida dessas variáveis ​​é igual à vida do processo Python. Portanto, o número de referências a objetos referenciados por variáveis ​​globais nunca cai para zero.

Variáveis ​​declaradas dentro de um bloco (função, classe) têm visibilidade local (ou seja, são visíveis apenas dentro do bloco). Assim que o intérprete python sai do bloco, ele destrói todos os links criados pelas variáveis ​​locais dentro dele.

Você sempre pode verificar o número de links usando a função sys.getrefcount .

Exemplo de um contador de links:

 foo = [] # 2 ,    foo    getrefcount print(sys.getrefcount(foo)) def bar(a): # 4  #  foo,   (a), getrefcount       print(sys.getrefcount(a)) bar(foo) # 2 ,      print(sys.getrefcount(foo)) 

O principal motivo pelo qual o intérprete padrão (CPython) usa um contador de referência é histórico. Atualmente, há muito debate sobre essa abordagem. Algumas pessoas acreditam que um coletor de lixo pode ser muito mais eficiente sem um algoritmo de contagem de links. Esse algoritmo tem muitos problemas, como links circulares, threads de bloqueio e sobrecarga adicional para memória e CPU.

A principal vantagem desse algoritmo é que os objetos são excluídos imediatamente assim que não são necessários.

Coletor de lixo opcional


Por que precisamos de um algoritmo adicional quando já temos contagem de referência?

Infelizmente, o algoritmo clássico de contagem de links tem uma grande desvantagem - ele não sabe como encontrar links circulares. Os loopbacks ocorrem quando um ou mais objetos fazem referência um ao outro.

Dois exemplos:

imagem

Como você pode ver, o primeiro objeto se refere a si mesmo, enquanto o object1 e o object2 referem um ao outro. Para esses objetos, a contagem de referência será sempre 1.

Demonstração do Python:

 import gc #  ctypes        class PyObject(ctypes.Structure): _fields_ = [("refcnt", ctypes.c_long)] gc.disable() #   GC lst = [] lst.append(lst) #    lst lst_address = id(lst) #   lst del lst object_1 = {} object_2 = {} object_1['obj2'] = object_2 object_2['obj1'] = object_1 obj_address = id(object_1) #   del object_1, object_2 #          # gc.collect() #    print(PyObject.from_address(obj_address).refcnt) print(PyObject.from_address(lst_address).refcnt) 

No exemplo acima, a instrução del remove referências aos nossos objetos (não aos objetos em si). Depois que o Python executa uma instrução del, esses objetos ficam inacessíveis a partir do código Python. No entanto, com o módulo gc desativado, eles ainda permanecerão na memória, pois eles tinham referências circulares e seu contador ainda é um. Você pode explorar visualmente esses relacionamentos usando a biblioteca objgraph .

Para corrigir esse problema, um algoritmo adicional conhecido como módulo gc foi adicionado no Python 1.5. A única tarefa é a remoção de objetos cíclicos aos quais não há mais acesso do código.

Os loopbacks podem ocorrer apenas em objetos "contêiner". I.e. em objetos que podem armazenar outros objetos, como listas, dicionários, classes e tuplas. O GC não controla tipos simples e imutáveis, exceto as tuplas. Algumas tuplas e dicionários também são excluídos da lista de rastreamento quando determinadas condições são atendidas. Com todos os outros objetos, o algoritmo de contagem de referência é garantido.

Quando o GC é acionado


Diferentemente do algoritmo de contagem de referência, o GC cíclico não funciona em tempo real e é executado periodicamente. Cada execução do coletor cria micro-pausas no código; portanto, o CPython (intérprete padrão) usa várias heurísticas para determinar a frequência do coletor de lixo.

O coletor de lixo cíclico divide todos os objetos em 3 gerações (gerações). Novos objetos caem na primeira geração. Se a nova instalação sobreviver ao processo de coleta de lixo, ela será movida para a próxima geração. Quanto maior a geração, menos frequentemente ela é verificada quanto a lixo. Como os novos objetos geralmente têm uma vida útil muito curta (são temporários), faz sentido entrevistá-los com mais frequência do que aqueles que já passaram por vários estágios da coleta de lixo.

Cada geração possui um contador especial e um limite de resposta, ao atingir o qual o processo de coleta de lixo é acionado. Cada contador armazena o número de alocações menos o número de desalocações em uma determinada geração. Assim que qualquer objeto contêiner é criado no Python, ele verifica esses contadores. Se as condições funcionarem, o processo de coleta de lixo será iniciado.

Se várias ou mais gerações ultrapassaram o limite de uma vez, a geração mais antiga é selecionada. Isso se deve ao fato de que as gerações mais antigas também examinam todas as anteriores. Para reduzir o número de pausas na coleta de lixo para objetos de vida longa, a geração mais antiga possui um conjunto adicional de condições .

Os limites padrão para gerações são definidos como 700, 10 10 respectivamente, mas você sempre pode alterá-los usando as gc.get_threshold gc.set_threshold .

Algoritmo de pesquisa de loop


Uma descrição completa do algoritmo de busca de loop exigirá um artigo separado. Em resumo, o GC itera sobre cada objeto das gerações selecionadas e remove temporariamente todos os links de um único objeto (todos os links aos quais esse objeto se refere). Após uma passagem completa, todos os objetos com uma contagem de links menor que dois são considerados inacessíveis do python e podem ser excluídos.

Para uma compreensão mais profunda, recomendo a leitura (a nota do tradutor: material em inglês) da descrição original do algoritmo de Neil Schemenauer e da função de collect das fontes do CPython . Uma descrição do Quora e uma publicação sobre o coletor de lixo também podem ser úteis.

Vale ressaltar que o problema com os destruidores descrito na descrição original do algoritmo foi corrigido desde o Python 3.4 (mais detalhes no PEP 442 ).

Dicas de otimização


Os loops geralmente ocorrem em tarefas da vida real; eles podem ser encontrados em problemas com gráficos, listas vinculadas ou em estruturas de dados em que você precisa acompanhar as relações entre os objetos. Se o seu programa tiver uma carga alta e exigir atrasos, se possível, é melhor evitar os loops.

Nos locais em que você usa conscientemente links circulares, pode usar links "fracos". Links fracos são implementados no módulo weakref e, diferentemente dos links regulares, não afetam o contador de links de forma alguma. Se o objeto com referências fracas acabar sendo excluído, None retornado.

Em alguns casos, é útil desativar a compilação automática pelo módulo gc e chamá-lo manualmente. Para fazer isso, basta chamar gc.disable() e depois chamar gc.collect() manualmente.

Como encontrar e depurar links circulares


Os loops de depuração podem ser dolorosos, especialmente se o seu código usar muitos módulos de terceiros.

O módulo gc fornece funções auxiliares que podem ajudar na depuração. Se os parâmetros do GC estiverem definidos para o sinalizador DEBUG_SAVEALL , todos os objetos inacessíveis serão adicionados à lista gc.garbage .

 import gc gc.set_debug(gc.DEBUG_SAVEALL) print(gc.get_count()) lst = [] lst.append(lst) list_id = id(lst) del lst gc.collect() for item in gc.garbage: print(item) assert list_id == id(item) 

Depois de identificar o problema, ele pode ser visualizado usando o objgraph .

imagem

Conclusão

O principal processo de coleta de lixo é realizado por um algoritmo de contagem de links, que é muito simples e não possui configurações. Um algoritmo adicional é usado apenas para pesquisar e excluir objetos com referências circulares.

Você não deve se envolver na otimização prematura do código do coletor de lixo; na prática, problemas com a coleta de lixo são bastante raros.

PS: Eu sou o autor deste artigo, você pode fazer qualquer pergunta.

Source: https://habr.com/ru/post/pt417215/


All Articles