artigo Descobrindo o Profiling de Aplicações Java com JProfiler Aprenda como encontrar problemas de performance na sua aplicação com o JProfiler. Muitas vezes, nos deparamos com problemas de performance nas aplicações que desenvolvemos ou realizamos manutenções. A maior parte dos problemas está relacionada com a má utilização ou insuficiência de memória. A solução imediata de muitos é aumentar a memória alocada para a aplicação, ou ainda, o número de nós do cluster do servidor de aplicação e finalmente o hardware disponível. Isso tudo sem ao menos realizar uma avaliação prévia do seu comportamento, buscando a causa-raiz dos problemas. Este artigo irá apresentar a técnica de profiling, que visa analisar e identificar problemas de performance em aplicações Java, utilizando a ferramenta JProfiler. J Diogenes Buarque Ianakiara ([email protected]. br): Está cursando o último semestre de Análise e Desenvolvimento de Sistemas na FIAP. Trabalha com Java desde 2006. Atualmente é analista de desempenho na Inmetrics. ava Profiling é a técnica que visa analisar o desempenho de aplicações coletando métricas e apresentando em tempo real a porcentagem de utilização de threads, carga de objetos e tempo de resposta dos métodos, possibilitando assim a identificação de gargalos e oportunidades de otimização na aplicação. Neste artigo apresentaremos um exemplo de profiling em uma aplicação remota com a ferramenta JProfiler. Vamos desenvolver um EJB simples com três serviços, que simularão um memory leak e um problema de consumo de cpu. Não abordaremos os conceitos de EJB ou padrões de projetos para não fugir do tema. Assim, iremos configurar o ambiente e o servidor de aplicação para que possamos realizar o profiling. Após a instalação, serão apresentadas as principais funcionalidades da ferra- 68 www.mundoj.com.br "SUJHPt%FTDPCSJOEPP1SPmMJOHEF"QMJDBÎÜFT+BWBDPN+1SPmMFS menta e o exemplo de uso de cada uma delas. O JProfiler irá se conectar remotamente à aplicação desenvolvida e iremos por fim realizar a análise e identificar o problema da aplicação. selecione a plataforma remota. No nosso caso, selecionaremos “Linux/ AMD64”, como mostra a figura 3. O memory leak, ou vazamento de memória, ocorre quando algum componente da nossa aplicação está utilizando uma determinada quantidade de memória para realizar alguma operação e, após finalizar esta operação, este componente não libera a memória utilizada. Geralmente isto ocorre devido a erros de programação e pode levar ao consumo total da memória. 'JHVSB4FMFDJPOBOEPPUJQPEFQSPmMJOHFP4JTUFNB0QFSBDJPOBM Instalação e configuração do JProfiler O JProfiler é uma ferramenta paga, porém a ej-technologies, fornecedora da ferramenta, oferece 10 dias de licença Trial, que até a data de escrita deste artigo está na versão 5.2.3. O download pode ser realizado no site http://www.ej-technologies.com/products/jprofiler/overview.html. Configuração 1.1 Uma vez terminada a instalação padrão, a primeira execução apresentará um wizard de inicialização rápida, para configurar o seu primeiro profiling. Na primeira tela, selecione a opção de profiling em uma aplicação local ou remota “An application Server, locally or remotely”, como mostra a figura 1. Na terceira etapa, existem três possibilidades: t "HVBSEBS QPS DPOFYÜFT BUSBWÏT EB (6* EP +1SPmMFS i8BJU GPS B connection from the JProfiler GUI”. Com esta opção, o JProfiler configura o servidor de aplicação para que ele aguarde o JProfiler se conectar antes de iniciar. Caso o JProfiler não se conecte, o servidor de aplicação não será inicializado). t *OJDJBS JNFEJBUBNFOUF i4UBSUVQ JNNFEJBUFMZ DPOOFDU MBUFS XJUI the JProfiler GUI”, O Jprofiler permite que o servidor inicie normalmente, possibilitando uma conexão futura com o JProfiler). t 3FBMJ[BS P QSPmMFS P÷JOF i+È B PQÎÍPi1SPmMF P÷JOF +1SPmMF (6* cannot connect” possibilita a realização do profiling em modo offline. Com esta opção, é possível configurar o servidor de aplicação para gravar os dados do profiling em um arquivo e assim possibilitar uma análise off-line). No nosso exemplo, vamos selecionar a opção iniciar imediatamente, conforme a figura 4. Figura 1. Selecionando o tipo de profiling. Em seguida, será necessário seguir 11 etapas para configurar o profiling. Na figura 2, é apresentada a primeira etapa, escolha o fornecedor e versão do servidor de aplicação. Nesse caso, selecione JBoss 5.x. Figura 4. Selecionando o tipo de inicialização do Servidor de Aplicação. Agora defina o endereço do servidor remoto, seguindo o exemplo da figura 5. 'JHVSB*OGPSNBOEPPFOEFSFÎPEPTFSWJEPSEFBQMJDBÎÍP Figura 2. Selecionando o servidor de aplicação. Na segunda etapa, se a aplicação estiver no mesmo servidor em que está instalado o JProfiler, selecione “On this computer”. Caso contrário, Na quinta etapa, o JProfiler deve ser instalado no servidor. Para isto, existe duas maneiras: 1 – Next, Next, Finish; 69 "SUJHPt%FTDPCSJOEPP1SPmMJOHEF"QMJDBÎÜFT+BWBDPN+1SPmMFS 2 – Copiar os arquivos de uma instalação préexistente para a mesma versão de SO. No próximo passo, devemos informar fornecedor, versão do Java e o modo de compilação, conforme a figura 9. O diretório da instalação deve ser informado nessa etapa, como mostra a figura 6. 'JHVSB*OGPSNBOEPBWFSTÍPEP+BWB 'JHVSB*OGPSNBOEPPEJSFUØSJPEP+1SPmMFSOPTFSWJEPS Na sexta etapa, precisamos informar o diretório onde estará o arquivo de configuração de acesso remoto ao JProfiler, e é de extrema importância para o sucesso do profiling. O nome deste arquivo é config.xml e ele será gerado automaticamente ao final deste wizard no diretório “C:\Documents and Settings\<usuario>\.jprofiler5\config.xml” para a plataforma Windows e “/home/<usuario>/.jprofiler5/” para a plataforma Linux/Unix. Utilize a figura 7 como referência. Como é apresentado na figura 10, o JProfiler utiliza a configuração default, para definir a porta de comunicação. A mesma pode ser alterada caso já esteja em uso. 'JHVSB*OGPSNBOEPBQPSUBEFDPNVOJDBÎÍP Aqui finalizamos nossa configuração, podemos visualizar as informações sobre este profiling e todos os cuidados que devemos ter antes de iniciar o profiling, como mostra a figura 11. Na 11ª etapa, você pode escolher entre iniciar imediatamente o profiling ou deixar para depois. 'JHVSB*OGPSNBOEPPEJSFUØSJPEP+1SPmMFSOPTFSWJEPS É possível observar na figura 8 que agora será necessário acessar o script de inicialização do servidor de aplicação para o JProfiler configurar os parâmetros de inicialização necessários. Para esta configuração, compartilhe o diretório do servidor aplicação com as devidas permissões de escrita. Assim, para o JBoss na plataforma Windows acesse o script run.bat no ou run.sh na plataforma Linux/Unix. Após clicar em Next o JProfiler irá gerar um novo script na mesma pasta indicada, chamado run_jprofiler.sh. Figura 11. Finalizando a configuração do profiling. Figura 8. Acessando o script de inicializar do servidor. 70 www.mundoj.com.br O processo de profiling geralmente é muito oneroso para o processador. Por isso é importante definir um período curto para a execução do profiling. Outro fator importante é o nível de detalhamento do profiling que deve ser definido antes de sua execução, o nível médio é recomendado para CPU e o baixo para memória. Caso seu servidor esteja com folga no uso de memória e CPU, estes níveis podem ser alterados conforme sua necessidade. "SUJHPt%FTDPCSJOEPP1SPmMJOHEF"QMJDBÎÜFT+BWBDPN+1SPmMFS Para podermos executar o profiling, vamos desenvolver uma miniaplicação que simulará o consumo de CPU e o Memory leak. Construiremos um EJB contendo três serviços. O primeiro serviço simulará um vazamento de memória, o segundo simulará o processamento de uma request simples e, por ultimo, teremos um método que terá a uma maior alocação de memória e processamento, entretanto não possuirá vazamento de memória. Implementação dos serviços 1.1 A Listagem 1 apresenta uma condição de errada no segundo "for", devido memory leak que o objeto string tem em cada iteração. Se fosse um registro de um banco de dados, este vazamento poderia resultar em uma falta de resposta da aplicação e esgotar rapidamente da memória disponível. Listagem 1. Implementação do método memoryLeak. Listagem 3. Implementação do método noMemoryLeak. public void noMemoryLeak (int size) { HashSet tmpStore = new HashSet(); for (int i=0; i<size; ++i) { String leakingUnit = new String(“Object: “ + i); tmpStore.add(leakingUnit); } } } Realizando o profiling e identificando o problema Primeiramente vamos abrir o JProfiler e selecionar a opção “Start Center”. Dentre as opções disponíveis, selecione a opção que você configurou no primeiro tópico, e clique em Start. Conforme a figura 12. public void memoryLeak(int iter, int count) { for (int i=0; i<iter; i++) { for (int n=0; n<count; n++) { memoryVector.add(Integer.toString(n+i)); } for (int n=count-1; n>0; n--) { memoryVector.removeElementAt(n); } } } A Listagem 2 apresenta um caso muito comum, quando uma requisição de entrada é mantida em uma hash table até que a mesma seja concluída, exceto neste exemplo, deixamos de propósito um erro de programação, no qual não removemos do hash table os objetos que foram inseridos anteriormente. Durante um período de tempo, a hash table terá um grande número de alocações, o que resultará em muitas colisões, bem como uma grande parte da pilha ocupada por entradas inúteis. Ambas as listagens são casos muito comuns que resultam em memory leaks. Figura 12. Selecionando a o profiling configurado. O wizard apresentado na figura 13 dá a possibilidade de configurar os níveis de detalhamento do profiling, iremos utilizar os níveis recomendados para não degradar o ambiente testado. Clique em ok para iniciar o profiling. Listagem 2. Implementação do método requestMemoryLeak. public void requestMemoryLeak(int iter) { Random requestQueue = new Random(); for (int i=0; i<iter; i++) { int newRequest = requestQueue.nextInt(); pendingRequests.add(new Integer(newRequest)); } } Podemos pensar que os locais de maior alocação de memória são os dois métodos anteriores. No entanto, isso não é verdade. Por exemplo, na Listagem 3, o método noMemoryLeak aloca uma grande quantidade de memória, porém tudo isso é coletado pelo GC (Garbage Collector), deixando a memória sempre disponível. Por outro lado, os outros dois métodos que não alocam muita memória estão causando constantemente memory leaks. 'JHVSB*OJDJBOEPPQSPmMJOH Agora vamos iniciar nossa aplicação cliente, que por sua vez consumirá os serviços do nosso componente MemoryLeakEJB. A Listagem 4 apresenta o código que simulará a utilização dos serviços disponíveis. Inicie a aplicação cliente. 71 "SUJHPt%FTDPCSJOEPP1SPmMJOHEF"QMJDBÎÜFT+BWBDPN+1SPmMFS Listagem 4. Implementação de uma aplicação Cliente. public static void main(String[] args) { Properties properties = new Properties(); properties.put(“java.naming.factory.initial”,”org.jnp.interfaces. NamingContextFactory”); properties.put(“java.naming.factory.url.pkgs”,”= org.jboss.naming:org.jnp.interfaces”); properties.put(“java.naming.provider.url”,”10.10.11.97:1099”); try { Context context = new InitialContext(properties); MemoryLeakRemote memoryLeakRemote = (MemoryLeakRemote) context.lookup(MemoryLeakBean. RemoteJNDIName); for (int i=0; true; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } sentado um botão ao qual será possível iniciar o monitoramento que desejamos. Para nosso exemplo, vamos utilizar apenas as aba inicial, All Objects e Allocation Call Tree. O ideal é avaliar um item por vez, seja ele memória, CPU, threads, ou snapshots. Para não degradar totalmente o ambiente. Uma que você iniciou uma avaliação é importante armazenar todas as evidências do que foi analisado, para futuras comparações, como também para um histórico do comportamento da aplicação. Após identificar que o componente MemoryLeakBean está na lista de objetos da aba All Objects, selecione a aba Allocation Call Tree. Esta aba apresentará uma árvore de utilização da memória. Como podemos observar na imagem da figura 15, os métodos do componente MemoryLeakBean consomem aproximadamente 50% do tempo de uso e alocação de memória, especialmente o método noLeak. System.out.println(“iteração nº: “ + i); memoryLeakRemote.slowlyLeakingVector(1000,10); memoryLeakRemote.leakingRequestLog(5000); memoryLeakRemote.noLeak(100000); } } catch (NamingException e) { e.printStackTrace(); throw new RuntimeException(e); } } Identificando problemas de vazamento de memória Neste momento, o JProfiler está conectado com o nosso servidor de aplicação. A primeira métrica que o JProfiler nos apresenta é a “Memory Views”. Estes dados dão a possibilidade de visualizar os níveis de agregação, podemos selecionar os níveis Class, Packages, J2EE Components. Para nosso exemplo, selecionaremos J2EE Components. Esta opção apresenta os componentes JEE mais utilizados no App Server, geralmente ele também apresenta componentes do próprio servidor de aplicação, porém podemos observar na figura 14 que dentre eles encontramos o nosso componente “MemoryLeakBean”. 'JHVSB7JTVBMJ[BOEPNÏUSJDBTEFNFNØSJBEBBCB"MMPDBUJPO$BMM5SFF Agora vamos selecionar a opção VM Telemetry Views. Esta opção apresentará gráficos de utilização da Heap, Thread e até CPU load. Ao selecionar pela primeira vez esta opção, já podemos observar um comportamento incomum da Heap. Notamos na figura 16 que este comportamento representa um memory leak ou vazamento de memória. 'JHVSB7JTVBMJ[BOEPPHSÈmDPEFNFNØSJB)FBQ Figura 14. Visualizando métricas de memória da Aba All Objects. As abas da parte inferior nos apresentam métricas diversificadas do uso de memória no servidor de aplicação. Ao selecionar uma aba, será apre72 www.mundoj.com.br Indo mais a fundo, vamos selecionar a aba GC Activity, que nos apresentará um gráfico da atividade do Garbage Collector. Podemos notar na figura 17 uma intensa atividade de GC, este comportamento se dá quando possuímos muitos novos objetos alocados, preenchendo todo espaço da Heap. Caso não haja mais memória para alocação, será lançado o “velho e conhecido” erro java.lang.OutOfMemory. "SUJHPt%FTDPCSJOEPP1SPmMJOHEF"QMJDBÎÜFT+BWBDPN+1SPmMFS Após identificar as causas de um problema de performance, deve-se analisar profundamente o problema para se obter a melhor solução. Muitas vezes estes problemas podem ser resolvidos com otimizações do código, porém nem sempre podemos alterar o mesmo, por exemplo, quando o código não nos pertence. Outros problemas podem ser resolvidos com o tuning da JVM. Tuning da JVM é o ato de otimizar o uso do garbage collector para que ele seja executado de forma mais eficiente, possibilitando assim um ganho de performance na aplicação. Figura 17. Visualizando o gráfico de atividade do GC. A opção CPU Views apresenta detalhadamente a árvore de uso de CPU. Podemos observar na figura 18 que 95% da utilização de CPU está alocada nos três métodos que estamos consumindo. Podemos filtrar ainda mais utilizando a opção Thread status. Esta opção fornece a visualização detalhada do uso de classes e métodos por estado de cada thread, seja ele blocked, running ou waiting. Não está no escopo do artigo se aprofundar em como resolver os problemas de performance ou como desenvolver um EJB. Todos os códigos estarão disponíveis no site http://www.mundoj.com.br. Saber mais Identificando problemas de consumo de CPU Na edição 35 da revista Mundoj o artigo “Conhecendo os parâmetros de configuração mais utilizados da JVM” apresentou técnicas de como otimizar a JVM. Considerações finais Neste artigo foi apresentadas algumas formas de identificar problemas de performance em aplicações Java utilizando a técnica de profiling. Esta técnica está fortemente ligada ao ciclo de vida das aplicações, uma vez que uma aplicação já nasce com problemas de performance, certamente estes problemas só irão aumentar. Assim buscamos apresentar não só os conceitos do profiling, mas também um exemplo prático de como identificar esses problemas. Figura 18. Visualizando a árvore de consumo de CPU. As abas Hot Spots apresentam tanto para memória ou CPU pontos que possivelmente podem ser críticos, porém estas métricas requerem um maior nível de detalhamento do profiling. Muitas vezes estas métricas podem apresentar resultados genéricos, como java.lang.String ou java. lang.Integer, entretanto, se você possuir um método muito oneroso para o sistema, ele com certeza aparecerá nesta análise. Como podemos observar o método noLeak da figura 19. No primeiro tópico apresentamos como configurar a ferramenta e construir um profiler remoto, seguindo, construímos duas aplicações, um EJB que simulou problemas de memory leak e cpu load e uma aplicação cliente que por sua vez consumiu todos os serviços do EJB. Assim, aprendemos como iniciar um profiling visualizando métricas de memória e cpu e como identificar não só o componente, mas também a classe e os métodos que apresentavam os problemas de performance. É importante salientar que apenas identificando o gargalo não necessariamente o problema estará resolvido. É preciso analisar e evidenciar para se chegar BVNBTPMVÎÍPFGFUJWBt Referências IUUQSFTPVSDFTFKUFDIOPMPHJFTDPNKQSPmMFSIFMQEPD IUUQXXXKBWBXPSMEDPNKBWBXPSMEKBWBRBRBDQVIUNM IUUQXXXJCNDPNEFWFMPQFSXPSLTSBUJPOBMMJCSBSZ@(VQUB1BMBOLJ IUUQXXXMBMJMVOBEFFKCUVUPSJBMKCPTTIUNM IUUQXXXNPVTFPWFSTUVEJPDPNCMPHQSPmMJOHFNKBWBDPNKQSPmMFS IUUQKBWBE[POFDPNBSUJDMFTKQSPmMFSZPVSKBWBDPEFDPVME Figura 19. Visualizando possíveis pontos de interesse. 73