Cuidado com o GC.Collect

Você já usou o comando GC.Collect? Há inúmeros casos que esse comando resolve problemas de memory leak.

Entretanto, esse procedimento é somente uma solução temporária e não resolve a real causa raiz. Pense: se o Garbage Collector (GC) do .NET faz a limpeza automática de memória, então por que rodá-lo manualmente?

Nesse artigo, vou abordar 3 pontos: Memory leak, Unmanaged code, Finalizers. Depois explico qual a relação com o GC.

Memory Leak

No C/C++, a memória alocada deve ser liberada posteriormente:

image

A possibilidade de memory leak (vazamento de memória) existe quando o programador deixa de chamar a função de free/delete correspondente ao bloco de memória.

Diferente do C/C++, consideramos o .NET como código gerenciado (managed code), pois não existe a necessidade de liberar explicitamente a memória. Não existe free ou delete. Podemos fazer alocações de memória sem nos preocuparmos com a forma de desalocação, pois toda “memória gerenciada” é liberada automaticamente pelo Garbage Collector. Isso diminui os riscos de memory leak.

Ainda fica a pergunta: por que rodar o GC.Collect manualmente em .NET?

Unmanaged Code

Você pode escrever um programa 100% em C#, mas sempre haverá trechos do runtime que dependem de recursos não-gerenciados. Por exemplo, veja os trechos abaixos:

image

StreamReader e SqlConnection são objetos gerenciado e, portanto, o GC faz a limpeza da memória associada. Entretanto, esses objetos usam recursos nativos do Sistema Operacional. No Windows, StreamReader possui uma referência a um handle de arquivo, porque internamente ele chama as funções do CreateFileEx do Kernel32.dll. De forma semelhante, o SqlConnection faz referência a uma DLL nativa (escrita em C++), que por sua vez abre sockets TCP via WinSock2.

Portanto, ambos os objetos dependem de recursos nativos e não-gerenciados, que não são da responsabilidade do Garbage Collector.

Finalizers

Os objetos do .NET possuem destrutores denominados de Finalizers. Esses métodos são chamados antes do Garbage Collector e são responsáveis por liberar a memória não-gerenciada relacionado ao unmanged code.

System.Object: Finalize method
https://msdn.microsoft.com/en-us/library/system.object.finalize

Finalizers (C# programming guide)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/destructors

O conceito de destrutor no C++ é bem conhecido. Sempre quando um objeto é desalocado, o destrutor é chamado e depois a memória é devolvida.

Entretanto, o comportamento no C# é ligeiramente diferente. O método Finalize roda em uma thread dedicada chamada de Finalizer Thread e não ocorre de forma determinística. Existe inclusive uma lista de objetos a serem finalizados:

image

Somente objetos que possuem recursos não-gerenciados entram na fila.

É importante lembrar que o Garbage Collector (GC) e a Finalizer Thread trabalham em paralelo. Ambos são não-determinísticos, ou seja, não é possível determinar o momento em que rodam. Entretanto, existe uma ordem: a remoção do objeto pelo GC ocorre somente depois de sua finalização.

Em muitos casos, os "problemas de memory leaks do .NET" estão associados a uma longa fila de finalização. Enquanto a thread de finalização está suspensa, essa fila pode crescer e aumentar o consumo de recursos não-gerenciados.

Conclusão

A conclusão é que o GC.Collect resolve o problema de memory leak associado com unmanaged resource/code, que não passou ainda pelo finalizer.

Portanto, o memory leak ocorre somente nessas condições:

  • Garbage collector mantém os objetos com finalização pendente
  • Objetos são finalizados de forma não-determinística (e pode demorar)
  • A fila de finalização cresce em número de objetos e causa o esgotamento de recurso

A solução é que normalmente adotar o Dispose pattern. Isso significa que os recursos não-gerenciados podem ser liberados de forma determinística através do método Dispose do objeto.

Assim como citado no exemplo anterior, os objetos StreamReader e SqlConnection possuem recursos nativos e que devem ser desalocados manualmente. Embora o finalizer da classe faça a liberação desses recursos, o ideal é sempre chamar o método Dispose depois de usar o objeto.

Nos próximos artigos, vou falar sobre o uso do try-finally para garantir que os recursos sejam sempre desalocados.