segunda-feira, 11 de agosto de 2008

Tagged under: , ,

Resolvendo problemas de performance (parte 2)

Continuando o nosso post sobre performance, vamos agora aprender a identificar memory leaks e como evitá-los.

Usando JConsole para monitorar a aplicação

A partir da versão 5 da plataforma Java, o JDK incluiu uma ferramenta para monitorar aplicações chamada JConsole. Essa ferramenta utiliza a tecnologia Java Management Extension (JMX) para coletar informações sobre performance e consumo de recursos do processo da JVM no sistema operacional. Essa ferramenta será substituída pela VisualVM a partir do JDK 6 update 7; na verdade, é o mesmo JConsole com uma interface gráfica melhorada baseada no Netbeans.

Para utilizar essa ferramenta, sua aplicação deve ser iniciada com o parâmetro que ativa o agente de monitoramento remoto (com.sun.management.jmxremote) . Exemplo:
C:/JDK_HOME/bin/java -Dcom.sun.management.jmxremote -jar MyApp.jar

Você também pode adicionar esse parâmetro para script que inicia o servidor Tomcat, JBoss ou qualquer outra aplicação Java que queira monitorar. A partir da versão 6 do JDK, esse parâmetro é opcional para utilizar o JConsole.

Para iniciar o JConsole, digite no console do sistema operacional:
C:/JDK_HOME/bin/jconsole

Se você estiver usando a JDK 6, verá a tela inicial como mostra a figura abaixo. Você pode escolher um processo Java local que esteja rodando na sua máquina ou um processo remoto (para conectar em processos remotos, consulte a documentação sobre JMX).


Após escolher o processo que você quer monitorar, aparecerá a tela abaixo com quatro gráficos. O primeiro gráfico, à esquerda e acima, mostra o consumo de memória. O segundo gráfico, à direita e acima, mostra a quantidade de threads rodando no seu processo. O terceiro gráfico, à esquerda e abaixo, mostra a quantidade de classes que foram carregadas para a memória (não confudir com instâncias delas que são os objetos). Finalmente, o quarto gráfico, à direita e abaixo, mostra o consumo da CPU pelo processo.


Observe que o gráfico de memória fica subindo e descendo a todo momento. Cada vez que a linha do gráfico cai significa que o Garbage Collector (GC) conseguiu remover objetos sem uso da memória. A figura abaixo aparecerá quando você clicar na aba Memory. Ela mostra o gráfico da memória, no caso em questão o consumo da área do heap, e permite a você ver cada espaço de memória individualmente, além de rodar o GC manualmente. É importante ressaltar que quando nós executamos o GC manualmente, uma coleção maior é que será executada e nunca uma coleção menor (discutimos esses assuntos no post anterior).


Agora que já temos a nossa ferramenta para enxergar o consumo de memória, aprenderemos como identificar se a queda de performance de nossa aplicação é na verdade um memory leak.

Identificando memory leaks

Memory leaks (gargalos de memória) são difíceis de enxergar sem uma ferramenta profiler adequada. Se a sua IDE já possui um profiler integrado, o melhor é utilizá-lo. Se não, a ferramenta JConsole poderá te ajudar nesse trabalho. Vamos considerar que você quer identificar memory leaks numa aplicação que você construiu usando um editor de texto simples (Notepad, Kwrite, etc).

Passos:
  1. Inicie a sua aplicação com o parâmetro que ativa o agente de monitoramento remoto;
  2. Inicie o JConsole e conecte-o ao processo da sua aplicação;
  3. Execute seu caso de uso (ou faça a requisição) para que a aplicação carregue todos os objetos necessários na memória;
  4. Observe o heap no gráfico do JConsole (ou capture a tela) e memorize a situação da memória; esse é o momento 1 - antes de executar seu caso de uso;
  5. Execute seu caso de uso novamente; as classes já deverão estar na memória mas os objetos serão criados novamente;
  6. Observe o heap no gráfico do JConsole (ou capture a tela) e memorize a situação da memória; esse é o momento 2 - depois de executar seu caso de uso;
  7. Compare os dois momentos e verifique se os objetos continuaram no heap após a execução do seu caso de uso;
  8. Force o GC e verifique novamente se a JVM conseguiu limpar os objetos sem uso.
Para melhorar a análise, realize os passos 5 e 6 várias vezes, comparando os momentos distintos (passo 7). Se o seu caso de uso termina e os objetos não estão sendo coletados após a realização dele, provavelmente você tem um memory leak em sua aplicação porque os objetos estão sendo retidos na memória por algum motivo.

O que pode causar um memory leak?

Entre as causas de um memory leak a mais comum de acontecer é a referência a um objeto que nunca é liberado, chamada também de object loitering. Para ilustrar esse efeito, observe o código a seguir que calcula o checksum de um arquivo.

 1: public class LeakyChecksum {
2: private byte[] byteArray;
3:
4: public synchronized int getFileChecksum(String fileName) {
5: int len = getFileSize(fileName);
6:
7: if (byteArray == null || byteArray.length < len)
8: byteArray = new byte[len];
9:
10: readFileContents(fileName, byteArray);
11: // calcula o checksum e retorna
12:
}
13: }

A decisão de transformar o buffer byteArray em variável de instância pode ter sido para melhorar o reuso do buffer e evitar criar esse objeto várias vezes na memória. O que aparentemente era a decisão correta se mostra errônea quando esse buffer nunca é liberado para a coleta de lixo porque ele sempre está alcancável pela aplicação (pelo menos até que a instância de LeakyChecksum seja coletada pelo GC). No pior caso, enquanto a instância é usada, o objeto LeakyChecksum permanentemente retêm um buffer tão grande quanto for o arquivo processado. Isso põe mais pressão no GC e requer coleções maiores frequentes. A solução para esse problema é deixar o buffer como sendo uma variável local ao escopo do método getFileChecksum(), assim o GC poderá completar o ciclo de vida desse objeto normalmente.

Outra forma de object loitering aparece quando arrays são usados para implementar estruturas de dados compostas. O código a seguir mostra uma implementação de uma pilha baseada em array.

 1: public class LeakyStack {
2: private Object[] elements = new Object[MAX_ELEMENTS];
3: private int size = 0;
4:
5: public void push(Object o) { elements[size++] = o; }
6:
7: public Object pop() {
8: if (size == 0)
9: throw new EmptyStackException();
10: else {
11: Object result = elements[--size];
12: // elements[size+1] = null;
13:
return result;
14: }
15: }
16: }

No método pop(), depois do ponteiro do topo ser decrementado, os elementos ainda mantêm a referência para o objeto que foi retirado da pilha. Isso significa que a referência para o objeto ainda está alcançável pela aplicação mesmo que o programa nunca mais a use novamente. Assim, o GC nunca coletará esse objeto até que a posição da pilha seja reusada em uma chamada futura ao método push(). A solução para esse memory leak é anular a referência do objeto que está sendo retirado da pilha, como mostra a linha comentada do código (linha 12).

Uma dica simples para evitar memory leaks é setar uma variável que não será mais utilizada no código para null. Assim, você está explicitamente dizendo ao GC que não quer mais o objeto e que ele pode ser coletado assim que o GC começar sua tarefa. Mas isso só não basta, temos que percorrer o nosso código observando o que pode atrapalhar o trabalho do GC. Lembre-se sempre: o GC só pode coletar objetos que não estão mais sendo usados, ou seja, referenciados. Se, de alguma forma, seu objeto ainda estiver sendo referenciado, ele é um candidato a permanecer no espaço Tenured por um longo tempo.

Outras dicas mais usuais para evitar problemas:
  • Quando usar coleções, depois que elas não forem mais úteis, chame o método clear(): isso fará com que os objetos sejam desreferenciados pela coleção;
  • Quando usar strings, depois que elas não forem mais úteis, referencie-as para null (por exemplo, str = null): isso fará com que seja explicitamente dito ao GC que você não as quer mais;
  • Utilize frameworks de cache de objetos como EhCache, principalmente se estiver usando o Hibernate;
  • Em sistemas web, sempre use pools de conexão: isso evita objetos de conexão com o banco de dados inúteis na memória.
  • Em sistemas web, evite o uso de escopo de sessão (session) e maximize o uso de escopo de requisição (request). Também é interessante baixar o tempo de expiração da sessão para evitar objetos presos no espaço Tenured.

Até a próxima

No próximo post, eu falarei dos tipos de Garbage Collector que existem na Sun JVM e quando usá-los para melhorar a performance de nossa aplicação, ou container. Até lá!

0 comentários: