Como usar o GC tuning em aplicações Java de alto throughput

1. Fundamentos do Garbage Collection e métricas de throughput

O Garbage Collection (GC) em Java gerencia automaticamente a memória, mas impacta diretamente o throughput de aplicações de alta carga. O ciclo de vida de objetos envolve alocação na geração jovem (Eden), promoção para sobrevivente e, eventualmente, para a geração velha. Durante a coleta, ocorrem pausas stop-the-world que interrompem a execução da aplicação.

As métricas essenciais para tuning são:
- Throughput: porcentagem do tempo gasto em trabalho útil vs. GC
- Latência: duração máxima das pausas
- Footprint de memória: quantidade de heap utilizada
- Frequência de GC: número de coletas por unidade de tempo

Quando o throughput cai abaixo de 95% ou as pausas excedem centenas de milissegundos, o tuning se torna necessário.

2. Escolhendo o Garbage Collector adequado para alto throughput

A escolha do GC depende do perfil da aplicação:

Coletor Throughput Latência Uso típico
Parallel GC Excelente Média Batch, processamento noturno
G1 GC Muito bom Baixa Servidores web, microsserviços
ZGC Bom Ultrabaixa Aplicações com requisitos de latência <10ms
Shenandoah Bom Ultrabaixa Sistemas interativos sensíveis

Para aplicações de alto throughput, o G1 GC é o equilíbrio ideal para a maioria dos cenários. O Parallel GC é superior apenas em ambientes batch com alocação previsível.

3. Configuração de parâmetros essenciais do heap e gerações

O primeiro passo é estabilizar o tamanho do heap para evitar expansão dinâmica:

-Xms8g -Xmx8g

A proporção entre gerações é crítica:

-XX:NewRatio=2     # Geração jovem com 1/3 do heap
-XX:SurvivorRatio=8  # Eden com 8x o tamanho de cada Survivor

Para balancear throughput e pausa no G1:

-XX:MaxGCPauseMillis=200  # Meta de pausa máxima
-XX:G1HeapRegionSize=4m   # Tamanho das regiões (1-32 MB)

Um exemplo completo para aplicação de alto throughput:

java -Xms8g -Xmx8g \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:G1HeapRegionSize=4m \
     -XX:NewRatio=2 \
     -XX:SurvivorRatio=8 \
     -jar aplicacao.jar

4. Tuning avançado do G1 GC para throughput máximo

Para aplicações com alocação intensa, ajuste os percentuais da geração jovem:

-XX:G1NewSizePercent=5    # Tamanho inicial da geração jovem (5% do heap)
-XX:G1MaxNewSizePercent=60 # Tamanho máximo (60% do heap)

O parâmetro InitiatingHeapOccupancyPercent controla quando o GC concorrente inicia:

-XX:InitiatingHeapOccupancyPercent=45  # Inicia quando heap ocupado atinge 45%

Para aumentar o throughput, reduza o número de threads de concorrência:

-XX:ConcGCThreads=2       # Threads concorrentes (default: 1/4 de ParallelGCThreads)
-XX:ParallelGCThreads=4   # Threads paralelas (default: número de CPUs)

Configuração otimizada para throughput máximo:

java -Xms16g -Xmx16g \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=500 \
     -XX:G1HeapRegionSize=8m \
     -XX:G1NewSizePercent=10 \
     -XX:G1MaxNewSizePercent=70 \
     -XX:InitiatingHeapOccupancyPercent=50 \
     -XX:ConcGCThreads=3 \
     -XX:ParallelGCThreads=6 \
     -Xlog:gc*:file=gc.log:time,uptime,level,tags \
     -jar aplicacao.jar

5. Estratégias de monitoramento e profiling de GC

Ative logs detalhados para análise:

-Xlog:gc*:file=gc.log:time,uptime,level,tags
-Xlog:gc+heap=debug
-Xlog:gc+promotion=info

Use ferramentas como GCeasy para interpretar logs. Exemplo de saída analisada:

Total GC time: 12.3s
Throughput: 97.2%
Max pause: 180ms
Average pause: 45ms
GC count: 245 (Young: 230, Mixed: 15)

Para monitoramento em tempo real com JFR:

-XX:+FlightRecorder \
-XX:StartFlightRecording=duration=60s,filename=recording.jfr

Métricas JMX importantes:
- jvm.gc.collection.time — tempo total de GC
- jvm.gc.collection.count — número de coletas
- jvm.memory.heap.used — heap utilizado

6. Mitigação de problemas comuns em aplicações de alta carga

Problema: Alocação excessiva de objetos temporários

Solução: Pooling de objetos e buffers reutilizáveis

// Exemplo conceitual de pooling
ObjectPool<ByteBuffer> bufferPool = new ObjectPool<>(() -> ByteBuffer.allocate(4096));
ByteBuffer buf = bufferPool.borrow();
// usar buffer
bufferPool.recycle(buf);

Problema: Promoção prematura para geração velha

Ajuste o tamanho da geração jovem:

-XX:G1NewSizePercent=15
-XX:G1MaxNewSizePercent=80

Problema: Fragmentação de heap

Aumente o tamanho das regiões G1:

-XX:G1HeapRegionSize=16m

7. Testes e validação de tuning em produção

Crie cenários de carga realistas com JMH para microbenchmarks:

# Comando JMH para testar throughput de alocação
java -jar benchmarks.jar -bm thrpt -f 1 -wi 5 -i 10

Use Gatling para testes de carga simulando picos:

# Simulação de 1000 usuários concorrentes
gatling.sh -s simulations.HighThroughputSimulation

Aplique mudanças graduais com flags JVM:

# Passo 1: Aumentar geração jovem
-XX:G1NewSizePercent=10 -XX:G1MaxNewSizePercent=60

# Passo 2: Ajustar threshold de concorrência
-XX:InitiatingHeapOccupancyPercent=45

# Passo 3: Reduzir threads concorrentes
-XX:ConcGCThreads=2

Monitore continuamente as métricas pós-tuning:

# Script de monitoramento
watch -n 5 'jstat -gcutil <pid> 1000 1'

Resultado esperado após tuning bem-sucedido:

Throughput: 98.5% (antes: 94.2%)
Max pause: 150ms (antes: 320ms)
GC frequency: 12/min (antes: 28/min)
Heap utilization: 65% (antes: 82%)

Referências