1. Introdução ao perfilamento
Perfilamento — é como um exame médico para o seu programa: não olhamos apenas a “temperatura” (monitoramento), mas procuramos onde o aplicativo “dói”, o que funciona lentamente e onde se consome memória ou recursos em excesso.
Perfilamento — é o processo de coleta e análise de informações sobre a execução do programa com o objetivo de identificar gargalos (bottlenecks) e trechos de código ineficientes. Diferentemente do monitoramento, que normalmente acompanha indicadores gerais (uso de CPU, memória, quantidade de threads), o perfilamento permite olhar por dentro: descobrir quais métodos são chamados com mais frequência, quanto tempo eles tomam, quantos objetos são criados e onde exatamente ocorre vazamento de memória.
Quando o perfilamento é realmente necessário?
- O aplicativo está “lento”, e não se sabe por quê.
- O consumo de memória aumentou de repente.
- Depois de uma atualização do código, algo passou a demorar mais.
- É preciso entender por que os recursos do servidor não são suficientes.
Aliás, quase todo desenvolvedor já otimizou a parte errada do código pelo menos uma vez. Por quê? Porque é praticamente impossível determinar o gargalo “no olho” — para isso é que serve o profiler.
Métricas principais do perfilamento
- Tempo de execução de métodos (perfilamento de CPU): Quais métodos consomem mais tempo? Onde o programa “gasta” CPU?
- Uso de memória (perfilamento de memória): Quais objetos são criados com mais frequência? Onde eles permanecem na memória por mais tempo do que o necessário?
- Quantidade de objetos: Não estamos criando objetos demais do mesmo tipo?
- Threads: Não há threads demais? Existem bloqueios (deadlock, contenção)?
- Chamadas de métodos: Qual é a profundidade da pilha? Não está ocorrendo recursão sem saída?
2. Ferramentas de perfilamento
No mundo Java há várias ferramentas clássicas (e gratuitas!) que permitem realizar perfilamento. Vamos ver as principais.
VisualVM
VisualVM — é uma ferramenta gratuita que vem com o JDK (a partir do JDK 6). Ela permite:
- Conectar-se a JVMs locais e remotas.
- Observar memória, threads, CPU e coleta de lixo.
- Fazer heap dump e analisá-lo.
- Perfilar o aplicativo por CPU e memória.
Como iniciar o VisualVM?
Geralmente ele está na pasta do JDK: <caminho_para_JDK>/bin/jvisualvm
Abra, selecione o processo do aplicativo Java — e você pode observar a sua vida como peixes em um aquário (só que aqui os peixes são objetos e threads).
JProfiler, YourKit
São ferramentas comerciais, porém muito poderosas. Elas permitem:
- Perfilar memória, CPU e threads.
- Analisar “instantâneos” de memória (heap dump).
- Encontrar vazamentos, bloqueios longos e métodos lentos.
- Integrar com IDE e CI/CD.
Para começar, VisualVM é suficiente, mas se você “crescer” para projetos maiores — considere essas ferramentas.
Java Flight Recorder (JFR)
JFR — é uma ferramenta embutida no JDK para coletar eventos da JVM. Ela é muito leve, quase não impacta o desempenho e permite coletar informações sobre:
- Tempo de execução de métodos.
- Coleta de lixo.
- Threads, bloqueios e erros.
JFR é excelente para produção, quando não é possível desacelerar o aplicativo.
3. Prática: perfilando um aplicativo simples
Vamos criar um mini-calculador que consegue executar cálculos longos e armazenar o histórico das operações (para termos laços, coleções e trabalho com memória).
Exemplo de código: “Calculador lento”
import java.util.ArrayList;
import java.util.List;
public class SlowCalculator {
private final List<String> history = new ArrayList<>();
public int add(int a, int b) {
simulateHeavyOperation();
int result = a + b;
history.add(a + " + " + b + " = " + result);
return result;
}
public int multiply(int a, int b) {
simulateHeavyOperation();
int result = a * b;
history.add(a + " * " + b + " = " + result);
return result;
}
public List<String> getHistory() {
return history;
}
// Simulação de operação "pesada"
private void simulateHeavyOperation() {
for (int i = 0; i < 5_000_000; i++) {
Math.sqrt(i);
}
}
}
E agora — a classe principal:
public class Main {
public static void main(String[] args) {
SlowCalculator calc = new SlowCalculator();
for (int i = 0; i < 10; i++) {
calc.add(i, i * 2);
calc.multiply(i, i + 5);
}
System.out.println("Histórico de operações:");
for (String entry : calc.getHistory()) {
System.out.println(entry);
}
}
}
Como perfilar este aplicativo?
- Compile e execute o aplicativo.
- Abra o VisualVM (jvisualvm).
- Encontre seu processo (geralmente pelo nome da classe Main).
- Vá até a aba CPU Profiler e clique em Start.
- Deixe o programa rodar (ou clique no botão lento novamente).
- Veja quais métodos consomem mais tempo.
Pergunta: Qual método, na sua opinião, será o mais “pesado”?
Resposta: Claro que simulateHeavyOperation() — afinal, ele roda um laço enorme de 5_000_000 iterações e chama Math.sqrt.
4. Problemas típicos de desempenho
Algoritmos lentos
A causa mais comum: escolha inadequada de algoritmo ou estrutura de dados. Por exemplo, busca em lista em vez de usar HashMap, ou ordenação por “bubble sort” em vez de quicksort.
Exemplo:
// Busca lenta
for (String s : list) {
if (s.equals("target")) {
// encontrado
}
}
É melhor usar Set ou Map para busca rápida.
Vazamentos de memória
Vazamento de memória — é a situação em que objetos permanecem “vivos” (existem referências para eles), embora não sejam mais necessários. Isso leva ao aumento do consumo de memória e, no fim, a OutOfMemoryError.
public class MemoryLeakDemo {
private static List<byte[]> leakyList = new ArrayList<>();
public static void main(String[] args) {
while (true) {
leakyList.add(new byte[1_000_000]); // 1 MB
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
}
}
}
Como encontrar vazamentos?
Faça um heap dump no VisualVM e veja quais objetos ocupam mais memória e por que existem referências para eles.
Criação excessiva de objetos
Se você cria muitos objetos do mesmo tipo dentro de um laço, isso não só sobrecarrega o coletor de lixo, como também pode deixar o aplicativo mais lento.
for (int i = 0; i < 1_000_000; i++) {
String s = new String("hello"); // ruim!
}
Melhor usar constantes ou o pool de strings (String pool).
Bloqueios de threads
Se várias threads disputam o mesmo recurso (por exemplo, um método sincronizado), isso pode levar a bloqueios e queda de desempenho.
public synchronized void doWork() {
// ...
}
Como encontrar?
Na aba Threads do VisualVM é possível ver quais threads “estão presas” e o motivo.
5. Abordagens de otimização
Meça primeiro, depois otimize
Regra principal da otimização: Não otimize o que não está lento.
Primeiro faça o perfilamento, encontre os “hot spots” e só depois altere o código. Às vezes o trecho mais “óbvio” ocupa apenas 1 % do tempo, e o verdadeiro “monstro” está em alguma biblioteca ou em um lugar inesperado.
Uso do profiler para encontrar hot spots
Hot spot — é um método ou trecho de código que consome a maior parte do tempo de execução do aplicativo.
No VisualVM isso é visível na aba CPU Profiler:
- Classifique os métodos por tempo de execução.
- Observe o stack trace: quem chama quem.
- Lembre-se de que, às vezes, o “culpado” não é o seu código, mas uma biblioteca ou até o próprio JDK.
Exemplos de otimização
Exemplo 1: Substituição de algoritmo
Se você descobriu que a maior parte do tempo é gasta buscando em uma lista, substitua List por HashSet.
Set<String> set = new HashSet<>(list);
if (set.contains("target")) {
// rápido!
}
Exemplo 2: Redução do número de alocações
Em vez de criar novos objetos em um laço, reaproveite-os ou use StringBuilder.
// Ruim:
for (int i = 0; i < 10000; i++) {
String s = "Resultado: " + i;
}
// Melhor:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.setLength(0);
sb.append("Resultado: ").append(i);
String s = sb.toString();
}
Exemplo 3: Cache
Se você notar que um método pesado é chamado muitas vezes com os mesmos parâmetros, use cache.
Map<Integer, Double> sqrtCache = new HashMap<>();
public double cachedSqrt(int x) {
return sqrtCache.computeIfAbsent(x, Math::sqrt);
}
6. Demonstração: acelerando nosso calculador
Problema: simulateHeavyOperation() consome muito tempo
Etapa 1. Perfilando
No VisualVM é visível que quase todo o tempo é gasto em Math.sqrt(i) dentro do laço de 5_000_000 iterações.
Etapa 2. Otimizando
Se isso for apenas uma simulação de carga — remova-a ou reduza o número de iterações.
Se for lógica de negócio real — pense se é possível:
- Fazer cache do resultado.
- Usar um algoritmo mais rápido.
- Mover os cálculos para uma thread separada (se não for crítico para o usuário).
Exemplo de otimização:
private void simulateHeavyOperation() {
// Era 5_000_000, agora 100_000
for (int i = 0; i < 100_000; i++) {
Math.sqrt(i);
}
}
Etapa 3. Verificando o resultado
Execute o perfilamento novamente — o programa ficou mais rápido e a carga de CPU diminuiu.
7. Visualização: processo de otimização
flowchart TD
A[Inicialização do aplicativo]
B["Perfilamento (VisualVM)"]
C[Identificação de gargalos]
D[Otimização de código]
E[Novo perfilamento]
F[Melhoria de desempenho]
A --> B --> C --> D --> E --> F
E --> C
8. Erros típicos ao perfilar e otimizar
Erro nº 1: Otimização “no olho”. Muito frequentemente os desenvolvedores começam a alterar o código sem medir onde está o problema de fato. Resultado: muito trabalho para ganho mínimo.
Erro nº 2: Perfilamento em condições “irreais”. É preciso perfilar com dados e carga próximos aos de produção. Perfilamento “no vazio” pode não revelar os problemas reais.
Erro nº 3: Ignorar vazamentos de memória. Se você não observar o heap dump e não analisar as referências, pode demorar a perceber que o programa está “inchando” e logo cairá.
Erro nº 4: Caça à micro-otimização. Não vale a pena gastar dias acelerando código que ocupa apenas 0,1 % do tempo de execução do aplicativo. Primeiro — os principais gargalos.
Erro nº 5: Desconsiderar threads e sincronização. Em aplicativos multithread, problemas de desempenho frequentemente estão ligados não a algoritmos, mas a bloqueios e esperas (synchronized, contenção).
Erro nº 6: Esquecer de perfilar depois das mudanças. Após a otimização, verifique obrigatoriamente o resultado: às vezes a “otimização” pode até desacelerar a execução!
GO TO FULL VERSION