Otimização de desempenho de aplicações Java: da teoria à prática
Java continua sendo uma das linguagens mais populares para criar sistemas corporativos e de alta carga. No entanto, mesmo uma aplicação bem escrita pode começar a ficar lenta com o tempo, consumir muita memória ou apresentar erros OutOfMemoryError. Neste artigo, vamos analisar os principais métodos de otimização de desempenho de aplicações Java que ajudarão você a tornar seu código mais rápido e eficiente. Você aprenderá como configurar a JVM, otimizar o gerenciamento de memória, evitar erros típicos e usar ferramentas modernas de profiling.
1. Profiling e identificação de gargalos
Antes de otimizar qualquer coisa, é preciso entender o que está causando a lentidão. Palpites intuitivos geralmente são errados — o gargalo pode não ser o método que parece lento. Portanto, o primeiro passo é o profiling. Use ferramentas como VisualVM, JProfiler ou o Java Flight Recorder (JFR) integrado.
Exemplo de inicialização de profiling via JFR a partir da linha de comando:
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr \ -jar my-application.jarApós a gravação, abra o arquivo no JDK Mission Control e observe:
- Perfil de CPU — quais métodos consomem mais tempo de processador.
- Memória — quais objetos são criados com mais frequência e ocupam mais espaço.
- Threads — se há bloqueios ou longas esperas.
Só após a análise dos dados você pode começar a fazer correções. Sem profiling, você corre o risco de otimizar algo que não é o problema.
2. Configuração da JVM e do garbage collector
A configuração correta da Máquina Virtual Java (JVM) pode aumentar o desempenho várias vezes. Os principais parâmetros a serem ajustados são:
- Tamanho do Heap — parâmetros
-Xms(tamanho inicial) e-Xmx(tamanho máximo). Não defina um tamanho inicial muito grande se a aplicação não precisar de tanta memória. - Escolha do garbage collector (GC) — aplicações modernas geralmente se beneficiam do G1GC (padrão desde o Java 9) ou do ZGC (para heaps grandes e baixa latência).
Exemplo de inicialização com configurações otimizadas:
java -Xms2g -Xmx4g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:+ParallelRefProcEnabled \ -jar my-app.jarObserve a flag -XX:MaxGCPauseMillis. Ela define o tempo alvo de pausa do garbage collector. Se sua aplicação for sensível a atrasos, tente definir entre 100-200 ms. No entanto, um valor muito baixo pode causar coletas frequentes e reduzir a taxa de transferência.
Também é útil ativar o log do GC para ver como o coletor está funcionando:
-Xlog:gc*:file=gc.log:time,uptime,level,tags3. Otimização de código: estruturas de dados e algoritmos eficientes
Mesmo a melhor configuração da JVM não salvará se o código não for otimizado. Aqui estão algumas recomendações específicas com exemplos:
3.1. Use as coleções corretas
Não use ArrayList se você precisar inserir elementos com frequência no início da lista — isso é O(n). É melhor usar LinkedList ou ArrayDeque. Para busca por chave, use HashMap ou TreeMap, e não listas.
Exemplo ruim:
List<String> names = new ArrayList<>();for (int i = 0; i < 100000; i++) { names.add(0, "Name" + i); // O(n) cada inserção}Exemplo bom:
List<String> names = new LinkedList<>();for (int i = 0; i < 100000; i++) { name