.NET - Conhecendo um pouco sobre o Garbage Collector(GC)


O gerenciamento de memória na plataforma .NET é um assunto muito controverso que pode gerar muitas discussões mas é muito importante que todo o desenvolvedor tenha pelo menos o conhecimento básico de como a plataforma gerencia a memória.

A .NET Framework é uma ambiente seguro e gerenciado para desenvolvimento e execução de aplicações ele gerencia todos os aspectos da execução de um programa aloca memória para armazenar as instruções e dados, fornece as permissões adequadas para a execução de sua aplicação, inicia e gerencia a execução da aplicação e gerencia a alocação  da memória liberando memória que não está sendo mais usada.

Os dois principais componentes da .NET Framework são : O Common Language Runtime (CLR)  e a .NET Framework Class Library (NFC).

Neste artigo vamos tratar um pouco sobre o mecanismo do Garbage Collector e do gerenciamento de memória na plataforma .NET.

O Garbage Collector ou coletor de lixo é um processo muito importante existente na CLR - Common Language RunTime.

A CLR exige que todos os recursos sejam alocados a partir da área de memória gerenciada chamada Heap e todos os objetos são automaticamente liberados quando não são mais necessários pela aplicação.

Quando a CLR libera os recursos ela o faz de uma forma não previsível chamando o Garbage Collector para realizar esta operação.

Em todos os tipos de programas uma variedade imensa de recursos é usada que seja na forma de recursos para acessar um banco de dados, espaço de tela, buffers de memória, conexões de rede, etc. Além destes recursos temos que no ambiente de programação orientada a objetos cada tipo usado é um recurso potencial disponível para ser usado pelo programa e a utilização desses recursos requer que memória seja alocada para representa o tipo.

De forma resumida as etapas cumpridas pela CLR para acessar esses recursos são:

Alocação de recursos

Quando um processo é iniciado , o runtime reserva uma área contigua de endereço de memória chamada de Heap que contém um ponteiro que indica onde o próximo objeto será alocado. Inicialmente o ponteiro é definido para o endereço base da região do endereço reservado.

Quando a aplicação cria um novo objeto usando o operador new a primeira operação feita é verificar se os bytes requeridos pelo novo objeto vão caber na área reservada, em caso positivo, o ponteiro aponta para o objeto na Heap, o construtor do objeto é chamado e o operador new retorna o endereço do objeto.

A figura ao lado mostra a Heap contendo dois objetos: A e B. O próximo objeto será alocado na área de memória
onde o ponteiro indica.

Quando a aplicação invoca o operador new para criar um objeto, pode não haver espaço suficiente na região alocada para o objeto. A heap detecta isto incluindo o tamanho do novo objeto para a área indica pelo ponteiro; se a área indicada esta além do espaço da região, então o heap esta cheio e a limpeza da memória deve ser feita.

Existem dois tipos de dados que são tratados de forma diferentes pelo CLR: Os tipos de valor e os tipos de referência.

Tipos de dados por valor

São considerados Tipos de dados por Valor os dados dos seguintes tipos : Byte , Integer , Single , Double , Boolean , Char , Struct , Enum

Uma variável declarada como um tipo de dados por valor é estruturada na memória para conter um valor diretamente. Vejamos :

- Um Integer é um tipo de dados por valor;

Dim intValor As Integer
intValor = 1

Depois de rodar o código acima o resultado é uma variável na memória chamada intValor que contém o valor 1 diretamente.

Os tipos de valor são alocados na área de memória chamada STACK.

Todo dado associado a um tipo por valor é alocado na memória Stack. Quando a função que é dona da variável termina sua execução a área de memória é liberada.

A STACK é que realiza o seu próprio gerenciamento usando os recursos da pilha, ou seja, o primeiro que entra é o último que sai. (FIRST-IN/LAST-OUT)

A Stack é a área da memória reservada para a aplicação rodar o programa.A  Stack é análoga a uma pilha de pratos. Pratos são colocados na pilha um sobre o outro. Sempre que um prato precisa ser retirado da pilha  é sempre o último que foi colocado nela que é retirado. Assim também acontece com as variáveis de programas.

Tipos de dados por referência

São considerados Tipos de dados por Referência os dados dos seguintes tipos : Class, Object, String, Interface, Delegate

Variáveis de tipo por referência existem nas duas áreas de memórias: STACK e Heap.

Quando esta variável é chamada por uma função ela retorna o endereço de memória do referido objeto a que ela representa.

Quando a função que a criou encerra suas ações a área de memória que guarda a referência ao objeto é liberada mas á área de memória aonde o objeto é armazenado não. Se outras variáveis fazem referência a este objeto ele permanece intacto.

Se o objeto não tem mais referência a ele, ele será eliminado na próxima vez que for executado pelo Common Language Runtime o Garbage Collection.

Uma variável declarada como um tipo de dados por referência é estruturada para conter uma referência para um objeto atual na HEAP. Uma referência é um endereço de memória onde um objeto existe na Heap.

- Um Button é um tipo de dados por referência

Dim mButton As Button

Depois de rodar o código acima o resultado é uma variável na memória chamada mButton que contém uma referência para um objeto button na Heap. Neste momento o valor da variável  mButton é null.

mButton = New Button()

A linha de código acima , depois de executada , fará com que a variável mButton contenha uma referência a um objeto Button na Heap. A referência é um valor inteiro que indica o endereço onde o objeto Button começa na Heap.

É na Heap que o Garbage Collector atua.

O coletor de lixo (Garbage Collector) conhecido também como GC ou gc, é usado para retirar objetos instanciados na memória mas que não estão sendo mais usados(referenciados). Este recurso, disponível na Linguagens .NET permite que você otimize o seu código, deixando ele mais rápido, e com isso ganhando produtividade O que o gc efetua é na verdade um gerenciamento automático de memória que automaticamente libera blocos de memória que não serão mais usados em uma aplicação.Vantagens do GC:
  • O programador não precisa se preocupar com detalhes de gerenciamento de memória, pois o sistema faz isto para ele;(Nada de ponteiros , malloc, free; usados por programadores C/C++)
  • O gerenciamento de memória efetuada pelo GC causa menos erros do que o feito por um programador; (Nada de "Vazamento de memória" causados por ponteiros não alocados na linguagem C)
  • O GC faz o serviço de gerenciar a memória melhor que um programador;

Memória Heap e Stack

A Heap é um pool de memória onde todas as instâncias dos tipos de referência (classes, arrays, etc.) são alocados e criados.

A área da memória heap é reservada para criar objetos reutilizáveis. O Common Language Runtime - CLR gerencia a alocação desta área de memória para objetos e controles e libera a memória de objetos e controles não utilizados através do Garbage Collection- GC.

Quando um objeto é inicializado , a CLR simplesmente aloca um endereço de memória na HEAP, e neste momento não há espaço de memória alocado para o objeto, porém um ponteiro para este endereço é mantido para que se ter uma referência para as próximas alocações. O espaço de memória é alocado somente quando um objeto é criado usando o operador new.

O gerenciamento da Heap é bem mais oneroso que o da Stack, devido à alocação de memória, acesso ao dado via referência, GC (Garbage Collection), etc.
Definir um objeto como do Tipo por Valor, na maioria das vezes, é a melhor escolha. Contudo, os Tipos por Valor têm uma desvantagem: Ocupam mais espaço na memória

A seguir temos uma figura que mostra como a memória Heap e gerenciada para alocação de novos objetos criados desde o inicio da declaração do tipo até a execução do operador new para os objetos A e B;

Quando não houver mais espaço na Heap a ser alocado para um novo objeto, ou o espaço disponível não for suficiente para acomodar o novo objeto, a CLR detecta o problema e chama o Coletor de lixo para realizar a liberação de memória.

Lembrando que muitos objetos na Heap estão vinculados a outros objetos formando uma cadeia de objetos vinculados. Nestes casos a referência para o primeiro objeto da cadeia é chamada de Root.

Todos os Roots são armazenados na STACK e identificam os espaços de alocação que referem-se a objetos na HEAP ou a objetos que são definidos por Null. A lista de Roots ativos é mantida pelo compilador Just-in-time (JIT) e pela CLR.

Cada aplicação possui um conjunto de Roots que identificam posições de armazenamento que referem-se a objetos na Heap.

Quando o Garbage Coletor(GC) é invocado ele assume que todos os objetos na Heap são lixo, ou seja, ele assume que nenhum root da aplicação faz referência a qualquer objeto na Heap. Então ele percorre os Roots e cria um gráfico de todos os objetos acessíveis a partir destes Roots e verifica as vinculações de cada objeto na Heap.

Depois que todos os Roots foram verificados , o gráfico que o GC criou contém um conjunto de todos os objetos acessíveis a partir dos Roots; qualquer objeto que não estiver contido no gráfico será considerado lixo e preparado para ser removido da memória.

Este processo pode ser resumido da seguinte forma:

Toda essa operação tem um impacto de desempenho mas lembre-se que o GC somente realiza esta operação quando a Heap estiver cheia.

Por fim a relação custo benefício entre a existência do GC e o esforço que você teria que fazer para gerenciar a memória por sua conta e risco pende para o lado da existência do GC.

Para tornar mais claro este processo vamos usar um trecho de código e explicar como o GC atua durante e após a sua execução:

Public Class Teste
    Public Shared Function ContaItensArray(ByVal args As String()) As Integer

        ' Cria um objeto ArrayList na Heap
        ' o objeto mArray esta agora na Root
        Dim mArray As New ArrayList()

        ' Cria 10000 objetos na Heap
        For x As Integer = 0 To 9999
            ' O objeto Object criado na Heap
            mArray.Add(New Object())
        Next
        ' Agora mArray esta na Root e dessa forma mArray esta acessível
        ' e os 10000 objetos para o qual ele aponta também estão acessíveis
        Return mArray.Count
        ' Depois que a última referencia a mArray é feita, mArray não esta mais na Root
        ' Agora temos 10001 objetos que não estão mais acessíveis
        ' e são considerados lixo, porém os objetos não são coletados 
        ' até que o GC seja invocado a realizar a operação de limpeza
    End Function
End Class

Eu sei é apenas .NET, mas eu gosto...

Referências:

José Carlos Macoratti