Fundamentos8 min

Garbage Collector e Performance: o impacto invisível

O GC pode ser o vilão silencioso da sua latência. Entenda como funciona e como minimizar seu impacto em performance.

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

  1. Picos periódicos de latência
  2. CPU alta sem código da aplicação usando
  3. Memória em dente de serra
  4. Aplicação congela momentaneamente

Diagnóstico

  1. Habilite logs de GC
  2. Correlacione pausas com picos de latência
  3. Analise frequência e duração
  4. Identifique se é minor ou major GC
  5. 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:

  1. Reduza alocações — menos lixo = menos coleta
  2. Escolha o GC adequado — diferentes GCs têm diferentes trade-offs
  3. Monitore continuamente — GC é uma fonte constante de problemas de latência
  4. Tune com cuidado — baseado em dados, não em intuição

O melhor GC é aquele que você nem percebe que está rodando.

garbage collectormemóriaJVMlatência
Compartilhar:
Read in English

Quer entender os limites da sua plataforma?

Entre em contato para uma avaliação de performance.

Fale Conosco