Em linguagens com gerenciamento automático de memória (Java, C#, Go, JavaScript), o Garbage Collector (GC) é responsável por liberar memória não utilizada. Embora seja uma conveniência enorme para desenvolvedores, o GC pode ser um vilão silencioso de performance.
Este artigo explora como o GC funciona, seu impacto em latência e throughput, e estratégias para minimizar problemas.
O GC é como um zelador que limpa enquanto você trabalha. Às vezes ele precisa parar tudo para uma faxina geral.
Como o Garbage Collector Funciona
Conceito básico
O GC identifica objetos na memória que não são mais referenciados e libera esse espaço para reutilização.
Alocação → Uso → Referência removida → GC identifica → Memória liberada
Tipos de coleta
Minor/Young GC
- Coleta objetos de vida curta
- Rápida (milissegundos)
- Frequente
Major/Full GC
- Coleta toda a heap
- Lenta (centenas de ms a segundos)
- Menos frequente
- Geralmente causa "stop-the-world"
Stop-the-World (STW)
Durante certas fases do GC, a aplicação é pausada completamente. Nenhum código da aplicação executa.
Tempo
│
│ App │ GC │ App │ GC │ App
│───────│████│───────────│████│──────
│STW │ │STW │
Essas pausas aparecem como picos de latência.
Impacto em Performance
Latência
Sintomas:
- Picos periódicos de latência
- p99 muito maior que p50
- Latência errática
Exemplo:
p50: 10ms
p95: 15ms
p99: 500ms ← Provavelmente GC
Throughput
O tempo gasto em GC é tempo não gasto processando requisições.
Regra geral:
- GC < 5% do tempo total: aceitável
- GC 5-10%: atenção
- GC > 10%: problema sério
Previsibilidade
Mesmo que a média seja boa, pausas de GC destroem a previsibilidade — essencial para SLOs de latência.
Fatores que Aumentam Pressão no GC
1. Alta taxa de alocação
Quanto mais objetos você cria, mais trabalho para o GC.
Vilões comuns:
- Strings intermediárias em loops
- Boxed primitives (Integer vs int)
- Streams e lambdas criando objetos temporários
- Serialização/deserialização
2. Objetos de vida média
Objetos que sobrevivem algumas coletas minor mas morrem antes de se tornarem permanentes criam trabalho extra.
3. Heap grande
Paradoxalmente, heaps muito grandes podem piorar o GC:
- Full GC demora mais
- Mais objetos para verificar
4. Fragmentação
Objetos de tamanhos variados deixam buracos na memória, forçando GCs mais frequentes.
Estratégias de Mitigação
1. Reduza alocações
Antes:
for (User user : users) {
String key = "user:" + user.getId(); // Nova String a cada iteração
cache.get(key);
}
Depois:
StringBuilder sb = new StringBuilder("user:");
int prefixLen = sb.length();
for (User user : users) {
sb.setLength(prefixLen);
sb.append(user.getId());
cache.get(sb.toString());
}
2. Object pooling
Reutilize objetos caros em vez de criar novos.
// Pool de buffers reutilizáveis
ByteBuffer buffer = bufferPool.acquire();
try {
// usa buffer
} finally {
bufferPool.release(buffer);
}
3. Use tipos primitivos
// Evite
List<Integer> numbers; // Cada Integer é um objeto
// Prefira
int[] numbers; // Array de primitivos
// ou use bibliotecas como Eclipse Collections, Trove
4. Escolha o GC certo
JVM oferece múltiplos GCs:
| GC | Foco | Quando usar |
|---|---|---|
| G1 | Balanceado | Default, boa escolha geral |
| ZGC | Baixa latência | Aplicações sensíveis a pausas |
| Shenandoah | Baixa latência | Similar ao ZGC |
| Parallel | Throughput | Batch processing |
5. Tune o GC
Parâmetros comuns (JVM):
# Tamanho da heap
-Xms4g -Xmx4g
# Meta de pausa do G1
-XX:MaxGCPauseMillis=200
# Usar ZGC para baixa latência
-XX:+UseZGC
Atenção: tuning prematuro pode piorar as coisas. Primeiro meça, depois ajuste.
6. Monitore GC
Habilite logs de GC:
-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=10M
Métricas importantes:
- Frequência de GCs
- Duração de pausas
- Tempo total em GC
- Memória recuperada por ciclo
GC em Outras Linguagens
Go
Go tem GC concurrent com pausas muito curtas (< 1ms típico).
Otimizações:
- Usar sync.Pool para objetos temporários
- Pré-alocar slices quando tamanho é conhecido
- Evitar criar closures em loops quentes
Node.js (V8)
V8 tem GC geracional similar à JVM.
Otimizações:
- Evitar criar funções dentro de loops
- Reutilizar objetos quando possível
- Usar typed arrays para dados numéricos
.NET
.NET tem GC geracional com diferentes modos (Workstation, Server).
Otimizações:
- Usar structs para objetos pequenos e de vida curta
- Span
para evitar alocações - ArrayPool
para buffers
Identificando Problemas de GC
Sintomas
- Picos periódicos de latência
- CPU alta sem código da aplicação usando
- Memória em dente de serra
- Aplicação congela momentaneamente
Diagnóstico
- Habilite logs de GC
- Correlacione pausas com picos de latência
- Analise frequência e duração
- Identifique se é minor ou major GC
- Profile para encontrar fontes de alocação
Conclusão
O Garbage Collector é uma faca de dois gumes:
- Benefício: libera desenvolvedores de gerenciar memória manualmente
- Custo: pode causar pausas imprevisíveis e degradar performance
Para sistemas sensíveis a latência:
- Reduza alocações — menos lixo = menos coleta
- Escolha o GC adequado — diferentes GCs têm diferentes trade-offs
- Monitore continuamente — GC é uma fonte constante de problemas de latência
- Tune com cuidado — baseado em dados, não em intuição
O melhor GC é aquele que você nem percebe que está rodando.