.NET

3 out, 2012

O Garbage Collector .NET não elimina a preocupação com memória

Publicidade

Não sei se isso aconteceu com você, mas eu, que vim do C/C++ para C#.Net, estranhei muito essa coisa de não se preocupar com liberação de memória. Todos os exemplos C# que via simplesmente alocavam o objeto, guardavam-no numa variável local qualquer (na pilha), e depois deixavam a variável local ser liberada. E o objeto que ficou no heap? Bom, o Garbage Collector, nosso “big brother”, vai cuidar da gente e não vai deixar nenhum problema acontecer.

Mas a verdade é um pouco diferente. O ambiente gerenciado e o GC (Garbage Collector) nos ajudam muito, realmente, mas não podemos simplesmente esquecer da finalização dos objetos.

Há muitas considerações sobre boas práticas para gerência de memória em .NET. Veja por exemplo as dicas dadas nesse artigo. Mas para manter esse artigo mais enxuto, vou focar na questão da finalização dos objetos. E para isso, vou pular boa parte do embasamento teórico e dos porquês, e vou direto ao como fazer.

Primeiramente, precisamos entender que há dois tipos de recursos: os gerenciados (objetos .NET) e os não gerenciados (recursos externos, como arquivos abertos, conexão com bancos de dados, etc.). A primeira ideia de alguém que vem do C/C++ é implementar um finalizador, um método do tipo ~Classe(), que libera todos os recursos. Um dos problemas dessa estratégia é o não determinismo, pois você não tem como saber em que momento o GC vai realmente coletar e finalizar esse objeto. E é bom lembrar que o custo do GC coletar memória é alto, e sua estratégia é adiar isso o máximo possível.

Por exemplo, se você escolher implementar uma certa rotina de banco de dados em uma classe .NET, e resolver fechar a conexão no finalizador da classe, a rotina será concluída, a variável que aponta para o objeto será liberada, mas ainda assim o objeto estará “vivo”, aguardando um GC chamar seu finalizador e liberá-lo. E isso irá manter a conexão com o banco aberta por um bom tempo.

Ou seja, no que diz respeito a recursos gerenciados que precisam ser liberados, precisamos seguir outra estratégia, algo que nos dê garantia do momento da finalização do objeto.

Outro preocupação é o tratamento de objetos gerenciados. Na verdade nós não conseguimos liberar memória deles explicitamente. Isso sempre será responsabilidade do GC, que fará isso na hora em que ele quiser. Mas nós devemos nos preocupar, dentre outras coisas, em finalizar os objetos gerenciados de forma adequada, especialmente objetos com tempo de vida maior (para mais detalhes, estude a estratégia do GC em separar os objetos em gerações) e com muitos ponteiros.

Uma das coisas que o GC precisa fazer quando vai fazer uma coleta, é verificar quais ponteiros sofreram alguma alteração e varrer todos os objetos, partindo dos “roots” (variáveis locais de um procedimento, por exemplo), para identificar quais objetos não estão mais sendo referenciados e são candidatos a liberação. Na verdade, toda vez que você faz objeto1.propriedade = objeto2 o “write barrier code” do GC é executado para controlar isso. Como um “full collection” é muito custoso para ser feito o tempo todo, o GC tem toda uma heurística para separar os objetos em gerações e diminuir esse esforço, mas detalhar isso aqui vai complicar muito o artigo.

Sendo mais pragmático, o que estou tentando dizer é que apesar de não liberarmos explicitamente um objeto gerenciado, ajudaria muito ter uma finalização explícita, não só para liberar recursos externos (não gerenciados), mas também para atribuir null a todos os ponteiros do objeto. Fazendo isso, o GC terá apenas um monte de objetos soltos, ao invés de uma complexa malha de objetos relacionados para inspecionar.

Se você se aprofundar nas boas práticas recomendadas para otimizar o trabalho do GC, vai ver muito mais coisas. Mas minha intenção nesse artigo é propor algo simples, que já vai ajudar muito a grande maioria dos desenvolvedores que desconhecem ou optaram por ignorar essas coisas, dada sua complexidade.

A recomendação da Microsoft para controlar explicitamente a finalização dos objetos é a implementação do padrão de projeto Disposable, implementando apropriadamente a interface IDisposable. Dessa forma, quando quiser liberar um objeto, basta chamar objeto.Dispose() .Embora não seja obrigatório, também recomendo fazer objeto = null.

O exemplo abaixo mostra como implementar o pattern de forma adequada. Os comentários no código exemplo explicam o porquê de cada coisa.

// Classe exemplo que implementa IDisposable
public class ClasseExemplo : IDisposable
{
//Apenas para exemplificar
private SqlConnection sqlConnection;

//Construtor da classe.
public ClasseExemplo()
{
//Inicializações de objetos gerenciados
sqlConnection = new SqlConnection();

//Inicializações de recursos não gerenciados como arquivos,
//conexoes, referências externas (IntPtr), etc.
}

#region Pattern Disposable

//Protege o programa de NullReference no caso de
//chamarem Dispose() mais de uma vez.
private bool finalizado = false;

// Implementação de IDisposable.
// Esse método nao deve ser virtual pois uma classe derivada
// não deve poder sobreescreve-lo.
public void Dispose()
{
//Chama o metodo finalizar que irá liberar os recursos,
//indicando que ele está sendo chamado do Dispose().
Finalizar(disposing: true);

// Objetos com finalizadores, ou seja, ~Classe(),
// impõem um custo adicional ao GC na coleta.
// Além disso, se já chamaos Dispose(), todos os
// recursos já foram liberados, e não precisaríamos
// chamar seu finalizador.
// Esse método diz para o GC ignorar a finalização
// desse objeto, pois ele já foi finalizado manualmente.
GC.SuppressFinalize(this);
}

//Finalizar(bool disposing) é executado em dois cenários
//distintos. Quando chamado via Dispose(), que por sua vez
//foi chamado direta ou indiretamente (via "using ..."
//por exemplo) pelo código do desenvolvedor. Ou quando chamado
//pelo finalizador ~Classe(), que por sua vez foi chamado
//pelo GC no momento da coleta. E isso só acontece se
//O Dispose() não foi chamado, por causa
//do GC.SuppressFinalize(this).
//A questão central aqui é que se ele for chamado pelo GC,
//não há como garantir o estado dos outros objetos gerenciados
//que porventura sejam referenciados por ele, pois o GC
//já pode ter liberado eles, e tentar referencia-los para
//chamar objeto.Dispose() pode ser desastroso. Logo, no
//caso de Finalizar() ter sido chamado pelo GC, queremos
//apenas liberar recursos NÃO GERENCIADOS, para que eles não
//fiquem bloqueados. Mas não devemos nos envolver com recursos
//gerenciados nesse momento.
private void Finalizar(bool disposing)
{
// Evita que Finalizar() seja chamado mais de uma vez
if (!this.finalizado)
{
//Se foi chamado por Dispose(),
//libera os recursos gerenciados
if (disposing)
{
// Libera recursos gerenciados.
sqlConnection.Dispose();
sqlConnection = null;
}

// Qualquer que seja o caso,
// libere aqui recursos não gerenciados,
// quando houver.

//Exemplo:
//CloseHandle(handle);
//handle = IntPtr.Zero;
}
finalizado = true;
}

// Implementa o destrutor C# para finalização da classe.
// Contudo esse finalizador só será chamado se o método Dispose
// do Pattern Disposable não for chamado pelo código do usuário.
~ClasseExemplo()
{
//Chama o metodo finalizar que irá liberar os recursos,
//indicando que ele NÃO está sendo chamado do Dispose(),
//ou seja, está sendo chamado do ~Classe().
Finalizar(disposing: false);
}

#endregion

}

As duas formas mais comuns de usar com segurança um objeto que implemente o pattern Disposable e finalizá-lo no final é:

var objeto = new ClasseExemplo();

try
{

//faz algo
}
finally
{
objeto.Dispose();
}

Ou utilizar o recurso sintático semanticamente equivalente do “using”:

using (var objeto = new ClasseExemplo())
{
//faz algo
}

Especialmente em aplicações com um tempo de vida longo (que ficam em execução por um tempo indeterminado), mais cedo ou mais tarde o limite de memória vai ser atingido e o GC vai precisar entrar em ação. E se as coisas não estiverem bem feitas, podemos enfrentar sérios problemas de performance e até de memória indisponível. Como disse antes, existem outras dicas importantes, mas fogem ao escopo desse artigo.

Espero ter ajudado. O GC é muito bom, mas não faz tudo de forma mágica e instantânea. Nós precisamos fazer a nossa parte para desenvolvermos aplicações robustas!