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:
- Inicie a sua aplicação com o parâmetro que ativa o agente de monitoramento remoto;
- Inicie o JConsole e conecte-o ao processo da sua aplicação;
- 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;
- 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;
- Execute seu caso de uso novamente; as classes já deverão estar na memória mas os objetos serão criados novamente;
- 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;
- Compare os dois momentos e verifique se os objetos continuaram no heap após a execução do seu caso de uso;
- Force o GC e verifique novamente se a JVM conseguiu limpar os objetos sem uso.
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:
Postar um comentário