Skip to main content

Garbage Collector

  • Garbage Collector

Garbage collection

As decisões do GC são baseadas em heurísticas

Explicação

As heurísticas no contexto do coletor de lixo (GC) em .NET referem-se a regras ou estratégias aproximadas usadas pelo coletor de lixo para tomar decisões sobre quando e como realizar a coleta de lixo. O GC em .NET usa várias heurísticas para otimizar o desempenho e a eficiência da coleta de lixo, levando em consideração fatores como a alocação de memória, a quantidade de memória não utilizada, a geração dos objetos, entre outros.

Aqui estão algumas das heurísticas comuns usadas pelo GC em .NET:

  1. Geração de objetos: O GC em .NET divide os objetos em diferentes gerações com base em sua idade. A maioria dos objetos é alocada na primeira geração (Geração 0). Heuristicamente, objetos que sobrevivem a uma ou mais coletas de Geração 0 são promovidos para gerações mais antigas. Isso é conhecido como a heurística da "hipótese da mortalidade infantil", que parte do pressuposto de que objetos que sobrevivem por um tempo tendem a ter uma vida mais longa.
  2. Tamanho da geração: O GC em .NET considera o tamanho da geração em relação ao tamanho total da memória disponível. Quando uma geração fica cheia até um certo limite, o GC é acionado para coletar objetos nessa geração. Essa heurística ajuda a evitar coletas frequentes em gerações mais jovens, onde a maioria dos objetos tem uma vida curta.
  3. Tempo decorrido: O GC também monitora o tempo decorrido desde a última coleta de lixo. Se o tempo decorrido atingir um limiar específico, o GC poderá ser acionado, mesmo que os outros critérios não tenham sido atendidos. Isso ajuda a evitar que a memória seja consumida indefinidamente sem coleta.
  4. Fragmentação de memória: O GC em .NET também considera a fragmentação de memória ao tomar decisões. A fragmentação ocorre quando a memória livre está dividida em pequenos espaços não contíguos. Em alguns casos, o GC pode ser acionado para compactar a memória e reduzir a fragmentação.
  5. Taxa de alocação: A taxa na qual objetos são alocados também é monitorada. Se a taxa de alocação for muito alta, isso pode indicar que a coleta de lixo é necessária para evitar que a memória seja esgotada rapidamente.

Essas heurísticas são usadas em conjunto para determinar quando o GC deve ser acionado e quais objetos devem ser coletados. O objetivo é equilibrar o desempenho e a eficiência, minimizando a interrupção do aplicativo causada pela coleta de lixo, ao mesmo tempo em que evita vazamentos de memória e uso excessivo de recursos. É importante notar que as implementações específicas do GC podem variar entre as versões do .NET e as configurações do aplicativo, e as heurísticas podem ser ajustadas para atender às necessidades de um cenário de uso específico.

O que acontece quando o GC é executado ?

Explicação

1-Suspensão : Suspende a execução de TODA a sua aplicação. Isso mesmo, seu aplicativo fica completamente congelado quando a execução do GC é iniciada.

Isso evita a simultaneidade de memória e garante que nada de novo seja colocado em qualquer lugar do heap. É por isso que o GC tem um grande impacto no desempenho do seu aplicativo e executá-lo o tempo todo pode não ser uma boa ideia.

Dependendo de como seu código está estruturado, o GC pode disparar centenas a milhares de vezes em poucos segundos.

GC é um código extraordinário e muito rápido, mas sejamos cautelosos ao usá-lo.

2-Varredura/Marcação: O GC inicia a varredura do heap em busca de objetos que perderam suas referências e, portanto, não serão mais necessários para sua aplicação e começa a removê-los.

Isso resulta em muitos espaços desalocados na memória, criando alta fragmentação, como podemos ver abaixo:

https://miro.medium.com/v2/resize:fit:630/1*N5qes7FSP4Jt2g8XjhI-RQ.png

Com a marcação e exclusão realizadas temos o seguinte cenário no heap:

https://miro.medium.com/v2/resize:fit:630/1*VjfO9H75dPSaaEWu-uBNAg.png

Você notou a quantidade de espaço não utilizado?

E se entre Object2 e Object4 quiséssemos alocar um novo objeto maior? Teria que ir até o fim da pilha e ficaríamos com esse espaço não utilizado.

Isso se não tivéssemos o terceiro passo.

3-Shrinking : Nesta etapa o GC começa a remover todos os espaços, movimentando todos esses objetos na memória (e isso é muito caro mas necessário) para que os espaços que estavam no meio sejam movidos após o último objeto alocado, e alocação de novos objetos pode ser sequencial.

4-Retomar: Finalmente nosso aplicativo descongela e tudo começa a funcionar novamente.

Como vimos, a execução do GC é necessária, embora seja algo muito caro e de alto impacto para o nosso sistema, então os engenheiros (não apenas C#) tiveram que pensar em maneiras de tornar esse processo mais inteligente para que pudesse corra com menos frequência.

GEN'S

Explicação

Gen0 - Gen0 é onde estão os objetos mais jovens. Eles são colocados nesta área, pois quanto mais jovem for um objeto, maior será a chance de ele não ser necessário no futuro.

Pense na quantidade de vezes que criamos variáveis de suporte ou realizamos uma simples consulta ao banco de dados para obter um valor que será usado exclusivamente para calcular outra coisa. Pense em como os objetos tendem a ser criados com mais frequência e a sobreviver por menos tempo.

É aqui que o GC é acionado principalmente para limpar e compactar áreas de memória. Agora, se um objeto sobreviver à limpeza Gen0, ele será promovido e alocado para Gen1.

Gen1 – A limpeza na Gen1 só é acionada quando a execução na Gen0 não consegue liberar memória suficiente. Funciona como uma área tampão entre Gen0 e Gen2.

Os objetos alocados em Gen0 e Gen1 são classificados como objetos efêmeros e é importante ressaltar que sempre que uma limpeza da Gen1 é acionada, a limpeza da Gen0 também é executada obrigatória e automaticamente.

Abaixo temos uma tabela com o tamanho da área das áreas de objetos efêmeros (Gen0 e Gen1), de acordo com a plataforma e tipo de GC (mais sobre isso depois):

https://miro.medium.com/v2/resize:fit:588/0*RS4fuh9UmupuFazR

E se o objeto sobreviver à Gen1, o que acontece com ele? Exatamente, vai para Gen2 que é a nossa área para objetos “antigos”.

Gen2 – Se o seu objeto sobreviveu à limpeza Gen0 e Gen1, é para lá que ele será movido.

Ah, e se o seu objeto for estático (ele nasce e morre junto com a aplicação) é aqui que ele será alocado também. Mas não apenas objetos promovidos e estáticos vivem na Gen2.

Gen2 também é responsável pela gestão de uma área crítica. Lembra que eu disse que logo após uma limpeza na pilha, é feito um processo de redução? Bem, nem sempre.

Isso só é realizado na área que chamamos de Small Objects Heap (SOH), que aloca apenas objetos menores que 85.000 bytes. Para objetos maiores, temos o Large Objects Heap (LOH).

Mas primeiro vamos falar um pouco mais sobre alocação de memória.

Se mesmo após a limpeza o heap ainda precisar crescer mais, um novo bloco de memória será alocado para armazenar esses objetos.

Mas apenas objetos efêmeros são movidos para lá. Neste caso, a área da geração 2 encolhe e os objetos efêmeros são movidos para esta nova área.

  • Pilha
    • Stack é um espaço contíguo na memória, ou seja, é uma área sequencial de 1 MB (sistemas de 32 bits) ou 4 MB (sistemas de 64 bits)
    • Sempre que chamamos um método um novo bloco é criado na pilha, esse bloco é chamado Stack Frame e contém informações sobre os parâmetros recebidos, o endereço de retorno da função chamadora e suas variáveis internas.
    • Propriedades do tipo valor de uma classe
      • Embora isto faça um pouco de sentido, esta afirmação está errada. Imagine que você tem uma classe chamada Book, esta classe Book possui uma propriedade inteira chamada ReleaseYear. Curiosamente este valor inteiro, apesar de ser um Tipo de Valor, é armazenado no Heap, pois está intimamente relacionado a um Tipo de Referência. Então sempre que você tiver uma propriedade de classe, por exemplo, esse valor vai para Heap não importa qual seja o seu tipo. Se o objeto Book não fosse uma Classe, mas sim um Struct, esse valor seria armazenado na Pilha, já que os Structs residem na Pilha.
  • Heap
    • Classes, Arrays, Strings e Delegados fazem parte
    • O tamanho do objeto na heap varia de acordo com a quantidade de propriedades