O que é coleta de lixo e como isso afeta o desempenho do seu programa?

Shutterstock / Smit
A coleta de lixo é um recurso de muitas linguagens como C # e Java. Embora o gerenciamento manual de memória como C ++ possa ser bastante rápido, a coleta de lixo automática melhora a qualidade de vida dos desenvolvedores. No entanto, é importante entender as implicações de desempenho de deixar o GC para fazer seu trabalho.
Pilha vs. pilha
Para entender o que um coletor de lixo faz, primeiro você precisa entender a diferença entre a memória armazenada na pilha e a memória armazenada no heap. Ambos são locais de memória específicos na memória atribuída ao seu programa, a partir da RAM disponível no seu computador.
O Stack é rápido e é usado para tipos de valor que têm um tamanho de byte fixo. É a mesma memória física que o heap, é claro, é apenas rápido porque é uma estrutura de dados primeiro a entrar, último a sair muito ordenada. Sempre que você cria uma variável local, ela a armazena na pilha e sempre que sua função sai e a variável sai do escopo, ela é automaticamente limpa da pilha.
Este é um processo muito rápido e torna as alocações de pilha basicamente gratuitas. Embora haja uma penalidade de desempenho, é tão barato quanto pode chegar.
O Heap, por outro lado, é usado para objetos grandes como listas e strings que são muito grandes para serem armazenados na pilha ou precisam ser armazenados muito depois que as funções saem do escopo, cujas alocações de pilha podem ’ t fazer por design. Sempre que você cria um novo objeto, está fazendo uma alocação de heap. Você provavelmente também está fazendo uma alocação de pilha, pois se estiver armazenando uma referência em uma variável local, essa variável local deve ser criada para apontar para a memória real do novo objeto.

O heap é um pouco mais lento, tanto para alocar memória quanto para remover. Isso se aplica a todas as linguagens que usam este modelo, com ou sem um coletor de lixo.
Limpando seu lixo
Claro, não é tão simples quanto apenas “ alocar uma vez e esquecer. ” Se nunca removêssemos a memória, teríamos um vazamento de memória, o que é muito ruim e consumirá rapidamente a RAM de sua máquina. Alocações como listas locais sairão do escopo imediatamente, mas, sem a limpeza, obstruirão o heap para sempre. Portanto, os programas devem ter uma maneira de limpar a memória que não é mais necessária.
Em linguagens de gerenciamento manual de memória como C ++, a memória é tratada, bem, manualmente. Você deve liberar memória manualmente e excluir os objetos que não está mais usando, usando uma referência ou um ponteiro para o local da memória desse objeto. Embora possa ser extremamente rápido, não é divertido codificar e pode levar a erros de memória e explorações. Este é um dos principais motivos pelos quais C ++ é visto como um dispositivo “ difícil ” linguagem de programação para aprender e codificar.
A alternativa ao gerenciamento manual é fazer com que a máquina faça isso para você automaticamente. Isso é o que chamamos de coleta de lixo.
Um coletor de lixo é executado em um thread de segundo plano e verifica periodicamente o heap e a pilha de seu aplicativo e procura por objetos que não têm mais nenhuma referência. Isso significa que o objeto não tem valor e pode ser removido com segurança sem afetar o programa.
Por exemplo, pegue o seguinte pseudocódigo, que cria e “ exclui ” um objeto
objeto refToObject = novo objeto (); refToObject = null;
Uma vez que refToObject não faz mais referência ao novo objeto () que foi criado, o coletor de lixo verá que o novo objeto está pendurado sem uma referência a ele de qualquer lugar e o removerá sempre que coletar o lixo na próxima vez.
O coletor de lixo também é muito inteligente e pode resolver dependências circulares. Por exemplo, se você tem dois objetos que fazem referência um ao outro, mas nada mais sabe sobre eles, isso é lixo. Na maioria dos casos, se um objeto não tiver uma cadeia de referência começando na raiz do programa e levando ao objeto, ele é lixo.
A coleta de lixo pode ser acionada a qualquer momento, geralmente:
- Quando o sistema está com pouca memória.
- A porcentagem de memória no heap ultrapassa um certo limite. Este limite é ajustado automaticamente e ocorre basicamente sempre que o GC vê que seu programa precisa de limpeza.
- Quando é acionado manualmente, como com GC. Collect ().
Impactos no desempenho
Claro, a coleta de lixo não é gratuita, de forma alguma. Se fosse, todas as línguas o usariam. O GC é lento, principalmente porque precisa pausar a execução do programa para coletar o lixo.
Pense nisso assim — sua CPU só pode funcionar em uma coisa de cada vez. Com o C ++, ele está sempre trabalhando no seu código, incluindo os bits que excluem a memória. Com um GC, seu programa não exclui a memória e executa até fazer algum lixo. Em seguida, ele é pausado e a CPU passa a trabalhar na coleta de lixo. Se estiver fazendo isso com frequência, pode diminuir o desempenho do aplicativo.
Normalmente, é bastante rápido, porém, geralmente menos de alguns milissegundos no máximo. Para . NET, isso depende de que tipo de memória está sendo limpo, já que mantém o controle da memória em diferentes “ gerações ”:
- Geração 0, a geração mais jovem que contém objetos de curta duração como variáveis temporárias.
- Geração 1, que atua como um buffer entre objetos de curto e longo prazo. Se um objeto sobreviver a uma tentativa de coleta de lixo, ele será “ promovido ” para uma geração superior.
- Geração 2, a última, que rastreia objetos de longo prazo.
O GC verificará os objetos em Gen0, Gen1 e Gen2. Uma vez que eles contêm apenas objetos temporários ou recém-criados, limpar Gen0 e Gen1 é geralmente muito rápido, mas Gen2 contém muita memória. Fazendo uma “ coleta de lixo completa ” pode ser muito mais lento do que coletas de lixo efêmeras.
Como acelerar o desempenho?
Então, o que você pode fazer para evitar isso? Bem, no final do dia, seu lixo deve ser recolhido. A única coisa real que você pode fazer é reduzir a quantidade de lixo que seu programa está jogando por aí.
Uma das principais maneiras de reduzir o lixo é utilizar o Object Pooling. O princípio básico por trás disso é que geralmente é mais rápido redefinir os objetos para o padrão do que fazer um novo e jogar o antigo fora.
Por exemplo, o código a seguir itera 10k vezes e faz uma nova lista a cada vez para fazer algo com ele. No entanto, isso é horrível no GC, então um método melhor é fazer uma lista grande e limpá-la depois que você terminar de usá-la e quiser uma nova.

Na prática, isso geralmente é feito com um “ Pool de objetos ” genérico; que gerencia uma lista de objetos que pode “ alugar ” ao seu programa. Quando seu código estiver pronto, ele libera o objeto de volta para o pool e o redefine, pronto para ser usado novamente quando solicitado.
Nenhum comentário