O que é memory leak?

Os programadores Java acham que com o advento do coletor de lixo (Garbage Collector) não é mais necessário nos preocuparmos com o gerenciamento da memória, diferentemente do que ocorre em algumas linguagens que exigem gerenciamento manual da memória, como C e C++.

Os programas normalmente apresentam "vazamentos de memória" ou "memory leak". Esse nome é dado quando uma porção de memória, alocada para uma determinada operação, não é liberada quando não é mais necessária. Programas Java também podem sofrer com vazamentos de memória, ao contrário do que muitos pensam. Esses problemas quando ocorrem podem desencadear uma série de problemas que são críticos para uma aplicação.

No restante do artigo será visto como se dá um vazamento de memória, quais são as origens mais comuns de ocorrerem vazamentos de memória e como podemos solucioná-los.

Identificando e solucionando memory leaks

A ocorrência de vazamentos de memória é quase sempre relacionada a erros de programação. Apesar de um programa continuar sendo executado, normalmente o vazamento de memória pode resultar em perda de memória devido a maior atividade do coletor de lixo ou a uma maior ocupação da memória. Além disso, vazamentos de memória podem causar problemas mais graves como paginação em disco e falhas no programa como OutOfMemoryError.

Para um exemplo simples de um vazamento de memória poderíamos ter o trecho de código a seguir:

Listagem 1: Exemplo de retirada de um elemento da pilha


public Object pop() {
  if (size == 0)
    throw new EmptyStackException();
  return elements[--size];
}

Esse simples trecho de código diminui o tamanho da pilha de elementos quando o usuário quer tirar um elemento da pilha. Nota-se que se a pilha tivesse 6 elementos, teríamos elemento[5] (lembrando que 0 possui um elemento). Quando o usuário retirasse um elemento da pilha com o método pop(), teria elements[4]. Aparentemente parece que nada de anormal está acontecendo neste código, no entanto, podemos notar que os objetos que são retirados da pilha não serão coletados pelo Garbage Collector, mesmo o programa não tendo mais referência a eles. Isso está acontecendo porque a pilha está guardando referências obsoletas desses objetos, ou seja, uma referência que nunca será eliminada. No caso do código acima, todos os objetos que estiverem depois da parte ativa da matriz (ou seja, depois de size) são objetos obsoletos. Um exemplo disso é quando tínhamos, por exemplo, elements[5] e retiramos um elemento ficando elements[4]. Ou seja, o programa não tem mais acesso ao antigo elemento elements[5], se um elemento for adicionado teremos um novo objeto criado e alocado em elements[5]. O antigo elements[5] apenas está ignorado, ele não foi completamente eliminado.

Outro grande problema é que além desses objetos estarem excluídos da coleta de lixo, o mesmo ocorrerá com todos os objetos referenciados por ele, o que também ocorrerá com os objetos do objetos referenciado e assim por diante. Esse tipo de situação derruba qualquer desempenho.

A solução para este tipo de problema é bastante simples: anular as referências quando elas se tornarem obsoletas. A versão corrigida do exemplo acima segue abaixo:

Listagem 2: Exemplo de retirada de um elemento da pilha anulando o objeto retirado


public Object pop() {
  if (size == 0)
    throw new EmptyStackException();

  Object result = elements[--size];
  elements[size] = null;
  return result;
}

Por isso sempre anule as referências. Com a anulação dessa referência o Garbage Collector sabe que existe um elemento nulo e pode coletá-lo, diferente de anteriormente que o elemento não era mais usado, mas ocupava espaço da memória. Caso um programador eliminasse uma referência por engano e tentasse usá-la, o programa avisaria com um NullPointerException, diferentemente da situação anterior em que o programa não avisa nada e silenciosamente vai acabando com a performance da aplicação.

Podemos verificar neste problema que uma possível fonte do vazamento de memória é quando o programador tenta gerenciar um pool de objetos. No caso anterior a matriz elements continha um pool de objetos e gerenciava a sua própria memória incluindo e excluindo objetos da memória. Portanto, sempre que uma classe gerenciar a sua própria memória, o programador deve ficar alerta a vazamentos de memória. Sempre que liberar um elemento, qualquer referência de objeto contida nele deve ser anulada.

Outras fontes de Memory Leaks (vazamentos de memória)

Uma outra fonte bastante comum de vazamentos de memória é a cache. Quando um elemento é adicionado na cache é bastante comum o programador esquecer que o objeto está lá e depois de muito tempo se tornou irrelevante.

Para solucionar o problema de vazamento de memória em cache poderíamos utilizar um Timer ou ScheduleThreadPoolExecutor rodando em segundo plano ou um efeito colateral através de inclusões de novas entradas no cache. Para o último caso poderíamos utilizar uma LinkedHashMap com o seu método removeEldestEntry.

Também devemos ficar atentos com vazamentos de memória utilizando listeners e outros retornos de chamadas.

Evitando vazamentos de memória (memory leaks)

Os vazamentos de memória podem existir nos mais diversos sistemas e estarem causando problemas que a anos não são descobertos. Uma forma de evitar esse tipo de problema é através de uma verificação cuidadosa do código fonte. A inspeção no código sempre revela bastante coisa, como vazamentos de memória. Outra forma é utilizando uma ferramenta de depuração conhecida como gerador de perfil de pilha.

Conclusão

Vimos neste artigo o que são vazamentos de memória e que esse tipo de problema pode ocorrer até mesmo em programas Java, que apesar de possuir o Garbage Collector, pode eventualmente sofrer problemas com vazamentos de memória devido a erros de programação. Verificamos quais são as fontes mais comuns de memory leak, como identificá-los e posteriormente solucioná-los. Além disso, vimos que podemos evitar esses vazamentos de memória manualmente ou com a ajuda de uma ferramenta automatizada.

Bibliografia