Um termo muito utilizado para a análise de aplicações é o Profiling. O profiling é usado para descrever o processo de medição do tempo de execução de métodos, para que assim possamos localizar e corrigir gargalos de desempenho. No contexto do Java esse termo é expandido ainda mais o que inclui a coleta de várias métricas e permite a depuração de threads e objetos em tempo de execução.

Existem diversas razões para utilizarmos os profilers nas aplicações Java, entre eles, para investigarmos o uso do heap e a frequência que está ocorrendo a coleta de lixo, pesquisar a alocação de objetos e referências para encontrar e corrigir vazamentos de memória, investigar a alocação e a sincronização de threads para encontrar problemas de bloqueio e de concorrência no acesso a dados, identificar métodos custosos, ou investigar uma aplicação em tempo de execução para que possamos entender a sua estrutura.

O profiling ocorre normalmente após a fase de desenvolvimento e os principais objetivos da sua utilização é melhorar o desempenho das aplicações, corrigir bugs de difícil localização e entender o que está acontecendo na aplicação enquanto ela executa.

Não deixe de conferir o Checklist Programador Java da DevMedia!

No restante do artigo veremos quais são os principais profilers e o que eles podem oferecer aos desenvolvedores, quais são as principais funcionalidades dessas ferramentas e como podemos testa-las na prática, e como podemos fazer algumas análises específicas para verificar problemas de desempenhos nas aplicações Java.

Profilers

Existem três profilers muito utilizados em Java e considerados os melhores pela comunidade, são eles: JProbe Suite (Quest Software), OptimizeIt Suite (Borland) e o JProfiler (Ej-Technologies).

Todos os três são considerados muito bons, porém o JProfiler tem como vantagem integrar todas as funcionalidades em um único aplicativo, ao invés de possuir ferramentas separadas para profiling, depuração de memória e depuração de threads. O JProbe tem como vantagem ser a primeira ferramenta do tipo e ainda é considerada a melhor por muitos desenvolvedores. O diferencial do JProbe em relação as outras ferramentas é a janela de grafo de heap que entre outras coisas permite navegar pelos grafos de objetos. O OptimizeIt é uma ferramenta da Borland bastante conhecida no mercado, sendo desenvolvida e mantida por uma equipe especializada. O grande problema do OptimizeIt é possuir integração a apenas algumas ferramentas de desenvolvimento.

Veremos a seguir mais informações sobre as ferramentas citadas.

JProbe

O JProbe Suite basicamente contém quatro ferramentas: JProbe Memory Debugger, JProbe Profiler, JProbe Threadalyzer e JProbe Coverage. O JProbe Memory Debugger ajuda o desenvolvedor a eliminar vazamentos de memória, reduzir o excesso de coleta de lixo e identificar objetos que estão segurando referências à outros objetos no heap. O JProbe Profiler combina uma interface com grafo de chamadas e coleta de informações para fornecer um diagnostico da performance da aplicação. O JProbe Threadalyzer é uma ferramenta poderosa para detecção de problemas com threads, deadlocks, race conditions, entre outros. Por fim, o JProbe Cobertura auxilia as equipes de desenvolvimento e a equipe de garantia da qualidade na localização de códigos não executados, tornando mais fácil avaliar a confiabilidade e a precisão dos testes de unidade.

Cada uma dessas ferramentas possui dois componentes: o JProbe Console e o JProbe Analysis. O JProbe Console é uma interface com o usuário executada como uma aplicação individual no JProbe Suite que pode analisar informações coletadas na sessão através do JProbe Analysis. O JProbe Analysis coleta dados, dentro do contexto de uma sessão, em uma aplicação Java stand-alone ou numa aplicação que esteja sendo executada num Servidor de Aplicação. O JProbe Analysis requer uma JVM que suporte a interface JVMPI que é uma inerface de profiling. O JProbe suporte as mais populares JVMs de uma variedade de sistemas operacionais.

As informações coletadas pelo JProbe Analysis podem ser visualizadas em tempo real ou armazenadas em arquivos para análise posterior.

A instalação do JProbe Suite pode ser realizada tanto em linha de comando quanto com uma interface GUI. O JProbe pode ser instalado em diversos Integrated Development Environment (IDE).

OptimizeIt

O Optimizeit permite aos desenvolvedores melhorarem o desempenho das suas aplicações Java, applets, servlets, JavaBeans, Enterprise JavaBeans (EJBs) e Java Server Pages (JSPs). Através de uma análise da JVM podemos verificar a alocação de memória da nossa aplicação ou o uso ineficiente de CPU.

Através de um wizard simples e rápido podemos instalar configurar o Optimizeit, não necessitamos recompilar o nosso programa com um compilador customizado ou modificar classes antes da sua execução. Assim, precisamos apenas executar o Optimizeit para iniciar os testes de desempenho. Após o executar o Optimizeit Profiler podemos acessar o Memory Profiler ou the CPU Profiler.

O Memory Profiler fornece uma visualização em tempo real de todas as classes usadas pelo programa e do número de instâncias alocadas. Usamos o Memory Profiler para verificar referências de objetos e instâncias alocadas. Além disso, o Memory Profiler possui filtros para que possamos focar em determinadas classes, controles para o garbage collection, grafos de referência, detecção de vazamentos de memória, entre outras funcionalidades.

O CPU Profiler exibe resltados de testes para cada thread ou um grupo de threads exibindo diversas estatísticas que podem ser filtradas.

O Optimizeit Profiler também possui um modo Virtual Machine Info que exibe e exporta gráficos que mostram informações de alto nível da JVM incluindo tamanho do heap, heap usado, número de threads, número de threads ocupadas e número de classes carregadas.

JProfiler

O JProfiler é uma ferramenta simples de ser utilizada e bastante poderosa. A configuração de sessões é bastante direta e a integração com ferramentas de terceiro é simples e a coleta dos dados é apresentada de forma bastante natural.

A ferramenta oferece profiling para JDBC, JPA/Hibernate e NoSQL (MongoDB, Cassandra e HBase), visto que problemas com chamadas a banco de dados se constituem nos principais problemas de desempenho das aplicações.

Também podemos visualizar uma árvore de chamadas realizadas aos componentes Java Enterprise Edition.

Informações de mais alto nível como chamadas RMI, arquivos, sockets e processos também são apresentadas ao JProfiler.

JProfiler também oferece suporte a informações sobre vazamentos de memória e informações do heap.

Equipe de garantia da qualidade podem utilizar o JProfiler como uma ferramenta para QA. A ferramenta oferece suporte a operações em linha de comando através de tarefas do ant permitindo exportar informações ou criar comparações

Assim como outros profilers o JProfiler é integrado a diferentes ambientes de desenvolvimento (fazendo profilindo durante a fase de desenvolvimento), servidores de aplicação e JVMs em geral tanto 32 bits quanto 64 bits

Outro ponto interessante do JProfiler é o seu baixo overhead, pois o JProfiler apenas grava informações quando necessitamos. Assim, podemos iniciar nossa aplicação com o agente do JProfiler e incluir o GUI JProfiler em um momento posterior. Quando não estamos gravando informações o overhead é extremamente baixo

Por fim, o JProfiler também permite executarmos um profiling na CPU verificando o status das threads, níveis de agregação, árvores de chamadas, grafo de chamadas, entre outras funcionalidades.

Outras ferramentas

Existem outras ferramentas que extraem essas informações já vistas das JVM em execução. As ferramentas apresentadas nesta seção são todas open source.

A Jmap é uma ferramenta desenvolvida pela Oracle que está presente na JVM e exibe um mapa dos objetos na memória além de detalhes do heap de um dado processo, de um arquivo ou de um servidor remoto. A VisualVM é outra ferramenta construída dentro da JVM que exibe informações visuais aos desenvolvedores como vazamentos de memória, espaço no heap, uso da CPU, entre outras funcionalidades. O problema da VisualVM é que ela é capaz de executar constantemente na JVM, não grava informações coletadas e não envia alertas. O BTrace é uma ferramenta mais completa que as anteriores que permite visualizarmos diversas informações como uso da memória geap, número de threads ativas, total de uso da CPU, chamadas SQL, operações na rede, entre outras.

Investigando Heap e Coleta de Lixo

O Garbage Collector é um recurso fornecido pela plataforma Java que elimina a necessidade do desenvolvedor liberar explicitamente objetos da memória. Porém, o custo disso é o overhead de desempenho quando a coleta de lixo é executada.

Utilizando o JProbe ou qualquer um dos outros profilers temos acesso a um gráfico resumido do heap para uma aplicação em execução. Com isso, podemos monitorar o tamanho total da memória alocada e da memória livre disponível.

Segue na Figura 1 um exemplo do JProbe investigando o uso do heap e a frequência do Garbagge Collector em uma aplicação que está sendo executada.

Informações sobre o heap no JProbe

Figura 1. Informações sobre o heap no JProbe.

Quando a quantidade de heap utilizada diminui é porque tivemos uma coleta de lixo realizada, ou seja, são procurados os objetos na memória que não são mais usados no programa e excluídos da memória, liberando assim mais espaço.
Podemos verificar que criar muitos objetos novos pode resultar em operações complexas e demoradas em que a JVM gastará tempo do processador no gerenciamento de memória, ao invés de realizar operações que constituem a lógica de negócio. O problema pode se agravar quando a JVM tem pouca memória livre, assim sendo o coletor de lixo precisa ser executado mais frequentemente para que mais memória esteja disponível. Podemos melhorar o desempenho da aplicação aumentando o tamanho do heap. Como exemplo, podemos executar uma aplicação e experimentar tamanho de 5MB e 16MB para o heap e comparar a frequência de execução do coletor de lixo. No entanto, isso não significa que pode haver uma melhora no desempenho, visto que, por vezes executar vários ciclos de coleta de lixo pode ser quase tão rápido quanto executar apenas um ciclo, mas teríamos que iterar por mais objetos. Por isso, é interessante experimentarmos valores para tamanhos iniciais e máximos do heap para uma aplicação específica, o teste é a única forma de validarmos confiavelmente a melhor configuração.

Encontrando e Corrigindo Vazamentos de Memória

A maioria das aplicações em Java possuem problemas com vazamentos de memória. Um vazamento de memória é quando a memória alocada não é liberada de volta ao pool. O Garbage Collector libera a memória mantida por objetos inacessíveis, porém se houver uma referência a um objeto, este objeto não estará elegível para coleta de lixo mesmo que nunca mais seja utilizado. Um exemplo disso é um objeto colocado em um array, se esse elemento nunca for removido ele permanecerá na memória. Se esses objetos permanecerem por muito tempo na memória eles podem consumir toda a memória e causar uma exceção OutOfMemory. Outro problema são os objetos persistentes (conexão com banco de dados ou arquivos temporários). Também quanto mais tempo os objetos permanecem na memória, mais custosa será a coleta de lixo, pois tem mais objetos a serem percorridos. Dessa forma, os profilers fornecem uma alternativa melhor de localizar esses objetos persistentes e identificar os objetos que estão dificultando a coleta de lixo. Com isso, podemos realizar uma pesquisa mais direcionada dentro do sistema para verificar o que está causando esse vazamento de memória.

Segue na Listagem 1 um exemplo de um programa que causa vazamentos de memória.

Listagem 1. Causando vazamentos de memória.


  public class FabricaDePontos {
           protected ArrayList pontos = new ArrayList();
           
           public Point criaPonto(int x, int y) {
                     Point ponto = new Point(x,y)
                     this.pontos.add(ponto);
                     return ponto;
           }
   
           public void removePonto(Point ponto) {
                     this.pontos.remove(ponto);
           }
   
           //Método que testa a criação de pontos
           public void testaImprimePontos() {
                     for (int i=0; i<5; i++) {
                              Point ponto = criaPonto(i,i);
                              System.out.println("Ponto = " + ponto);
                     }
           }
   
  }

Utilizando o JProbe podemos verificar a situação desse aplicativo. Se aumentarmos o tamanho do objeto ou o numero de iterações podemos exaurir a memória rapidamente.

Para investigar o gerenciamento de memória no código de exemplo devemos configurar o convertjava.profile.PointFactory como uma aplicação independente no JProbe Memory Debugger. Executando-a no JProbe, a visualização inicial mostra um resumo do heap em tempo de execução e uma visualização das classes. Inicialmente nenhum objeto java.awt.Point ainda foi criado, mas ao clicar no botão PRINT TEST POINTS na aplicação retornaremos à visualização de classes onde podemos verificar cinco instância de StringBuffer criadas. Com JProbe também podemos forçar a coletar de lixo e verificar que as instância de StringBuffer desapareceram e os pontos ainda estão na memória. Se clicarmos novamente nesse botão o número aumenta para 10. Isso confirma que temos vazamentos de memória e agora devemos descobrir porque os pontos estão persistindo na memória, seja através de uma inspeção no código fonte localizando onde as instâncias são criadas ou navegando pela árvore de referências em um profiler e localizando todos os caminhos na memória para a raiz. Outra visualização interessante que o JProbe disponibiliza é a Reference Graph. Uma forma de resolver esta situação seria adicionar o método "remove" após o "println" no método "testaImprimePontos".

Investigando a Alocação e a Sincronização de Threads

Aplicações multithreads possuem um alto risco devido aos problemas de sincronização, onde várias operações concorrentes podem ser realizadas ao mesmo tempo. Enquanto que se utilizarmos threads simples o código será executado na mesma ordem em que está escrito no código fonte. Como nas aplicações multithreads cada thread pode ser interrompida para permitir que o processador execute operações de outra thread temos que a ordem e os pontos de interrupção são praticamente aleatórios e não podem ser previstos.

Entre os problemas encontrados com as multithreads temos a concorrência de dados onde dois threads tentam acessar e modificar ao mesmo tempo um recurso compartilhado, deadlocks onde uma thread espera um lock adquirido por outra thread que nunca é liberado, e travamentos de threads onde as threads ficam esperando ser notificados, mas a notificação nunca chega.

Os profilers como o JProbe Threadalizes podem capturar e informar os vários tipos de concorrência de dados que podem ocorrer. Para problemas de deadlock o JProbe Threadalizer e o OptimizeIr Thread Debugger podem detectar e fornecer informações sobre deadlocks. O OptimizeIt inclusive fornece uma linha de tempo visual da execução da thread. Para testar o OptimizeIt podemos utilizar o exemplo abaixo e após isso executamos o OptimizeIt e configurarmos o ThreadTest como a classe principal, também devemos marcar a opção Auto-start Analyzer na caixa de diálogo de configurações da aplicação para que ele analise automaticamente a aplicação em execução a procura de problemas.

Segue na Listagem 2 um exemplo de um código em deadlock.

Listagem 2. Exemplo de código em deadlock.


  public Object lock1 = new Object();
  public Object lock2 = new Object();
   
  public void metodo1() {
           synchronized(lock1) {
                     synchronized(lock2) {
                              System.out.println(“teste”);
                     }
           }
  }
   
  public void metodo2() {
           synchronized(lock2) {
                     synchronized(lock1) {
                              System.out.println(“teste”);
                     }
           }
  }

Ao executarmos o JProbe podemos verificar todas as threads e a linha de tempo de seus status em execução. O verde indica que a thread está em execução, o amarelo indica que a thread está bloqueada enquanto espera um lock que está com outra thread, o vermelho é quando a thread aguarda uma notificação, e o roxo indica que a thread está bloqueada. Podemos verificar que após a execução do código cima as duas threads “metodo1” e “metodo2” ficam bloqueadas indefinidamente. Se utilizarmos o menu File -> Analyser -> Stop Recording podemos verificar a identificação de um deadlock detectado pela ferramenta em "Locking order mismatch". Se selecionarmos o item podemos verificar mais detalhes sobre o deadlock encontrado.

Os travamentos das threads também são capturados pelos profilers. Para testarmos esses travamentos podemos criar uma thread que chama o método wait() em um objeto, mas nenhum outro thread chama notify() no mesmo objeto.

Investigando Métodos Custosos

Apesar das melhorias nas JIT recentes, o fator principal para ganho de qualidade ainda é o design e a implementação da aplicação. Por isso a importância de conhecermos boas práticas e armadinhas comuns. Para ganharmos desempenho devemos inicialmente identificar onde está o gargalo, por isso à medida que desenvolvemos e testamos o código deve ser analisado com um profiler para identificarmos os métodos que estão levando maior tempo.

De forma geral, o profiling coleta diversas estatísticas de execução fornecendo uma ideia onde o tempo está sendo gasto. As métricas mais importantes e úteis produzidas pelo JProbe nesse caso é a “Method Time” onde temos o tempo gasto executando um dado método excluindo o tempo gasto nos método que ele chamou, o “Method Number of Calls” onde temos o número de vezes em que o método foi chamado, o “Avarage Method Time” onde temos o tempo médio que o método levou para ser executado, o “Cumulative Time” que fornece o tempo total gasto ao executar um dado método incluindo os que ele chamou e o “Avarage Objects per Method” em que tem-se o número médio de objetos criados dentro de um método (útil para sabermos como a GC pode estar influenciando no desempenho).

Bibliografia

[1] Vanhelsuwé, Laurence. Profiling the profilers: A comparative review of three commercial Java code profilers.
http://www.javaworld.com/article/2073674/build-ci-sdlc/profiling-the-profilers.html

[2] Tool Report: JProbe
http://www.javaperformancetuning.com/tools/jprobe/

[3] JProbe
http://java.quest.com/support/jprobe