Algoritmos de GC no Java
Compare os principais coletores de lixo do Java — Serial, Parallel, G1, ZGC e Shenandoah — e suas vantagens e desvantagens.
A JVM libera você de desalocar memória manualmente: um coletor de lixo (GC) é executado em segundo plano, encontra objetos que seu programa não pode mais alcançar e recupera o espaço deles. Mas "o coletor de lixo" não é uma coisa só. A JVM HotSpot fornece vários coletores, cada um fazendo um acordo diferente entre throughput (quanto da CPU vai para sua aplicação em vez de para o GC), latência (quanto tempo a aplicação fica pausada enquanto o GC trabalha) e footprint (quanto de memória o próprio coletor consome como overhead).
Escolher bem importa. Um job em lote que processa números durante a noite quer o máximo de throughput e não se preocupa com pausas; um sistema de trading ou um servidor web quer as pausas mais curtas possíveis, mesmo que o throughput total caia um pouco. Este capítulo compara os coletores de produção e mostra o que todos têm em comum: um objeto vive exatamente enquanto for alcançável.
Como os coletores decidem o que manter
Todo coletor HotSpot responde à mesma pergunta — quais objetos ainda estão em uso — da mesma forma: ele rastreia a alcançabilidade a partir de um conjunto de raízes de GC (variáveis locais na pilha, campos estáticos, threads ativas). Qualquer objeto alcançável a partir de uma raiz está vivo; todo o resto é lixo. Escopo e idade são irrelevantes; apenas a alcançabilidade conta. Para uma visão mais ampla de como isso se encaixa no runtime, veja Java Garbage Collection e Stack vs Heap.
public class Reachability {
public static void main(String[] args) {
String a = new String("kept"); // reachable via local variable 'a'
String b = new String("dropped"); // reachable via 'b'...
b = null; // ...until now: "dropped" is unreachable
System.out.println(a); // 'a' is still a GC root reference
}
}No momento em que b = null é executado, o objeto "dropped" não tem caminho a partir de nenhuma raiz e se torna elegível para coleta. O coletor pode recuperá-lo imediatamente, muito mais tarde ou — se o programa terminar antes — nunca. Você nunca chama free; simplesmente para de referenciar.
Layout geracional do heap
A maioria dos objetos Java morre jovem. Os coletores exploram isso com um heap geracional: novos objetos chegam na geração jovem (Eden mais dois espaços de sobrevivência), e objetos que sobrevivem a várias coletas são promovidos para a geração antiga. Coletar frequentemente a geração jovem, pequena e cheia de lixo, é barato; a grande geração antiga é coletada com muito menos frequência.
| Região | O que vive aqui | Com que frequência é coletada |
|---|---|---|
| Eden | Objetos recém-alocados | A cada GC menor |
| Survivor (S0/S1) | Objetos que sobreviveram a um GC menor | A cada GC menor |
| Old (tenured) | Objetos de longa duração, promovidos | GC maior / completo |
| Metaspace | Metadados de classe (fora do heap) | Ao descarregar classes |
Um GC menor limpa a geração jovem e é rápido; um GC maior ou completo toca a geração antiga e é a fonte das longas pausas que as pessoas temem.
Comparação dos coletores
O HotSpot permite escolher um coletor com uma única flag, e cada um é ajustado para um objetivo diferente. Raramente se muda o algoritmo no código — ele é definido na linha de comando.
java -XX:+UseSerialGC MyApp # single-threaded, tiny heaps
java -XX:+UseParallelGC MyApp # throughput-oriented, multi-threaded
java -XX:+UseG1GC MyApp # balanced, the default since Java 9
java -XX:+UseZGC MyApp # sub-millisecond pauses, huge heaps
java -XX:+UseShenandoahGC MyApp # low pause, concurrent compactionA tabela abaixo é o modelo mental a ser utilizado:
| Coletor | Ponto forte | Comportamento de pausa | Uso típico |
|---|---|---|---|
| Serial | O mais simples, baixo footprint | Stop-the-world, thread única | Heaps pequenos, containers, CLIs |
| Parallel | Maior throughput | Stop-the-world, múltiplas threads | Batch / processamento de dados |
| G1 | Equilibrado, previsível | Majoritariamente concorrente, pausa-alvo | Padrão de uso geral |
| ZGC | Latência muito baixa | Sub-milissegundo, concorrente | Heaps de vários GB a TB |
| Shenandoah | Latência muito baixa | Pausas independentes do tamanho do heap | Serviços responsivos |
O G1 ("Garbage-First") é o padrão a partir do Java 9. Ele divide o heap em regiões de tamanho igual e coleta as regiões com mais lixo primeiro, visando um objetivo de tempo de pausa definido com -XX:MaxGCPauseMillis=200.
Concorrente vs stop-the-world
O eixo crucial é quando as threads da aplicação precisam parar. Coletores stop-the-world (STW) (Serial, Parallel) pausam todas as threads da aplicação enquanto trabalham — simples e com alto throughput, mas a pausa cresce com o heap. Coletores concorrentes (ZGC, Shenandoah e a maior parte do G1) fazem a maior parte do trabalho enquanto suas threads continuam em execução, então as pausas permanecem curtas mesmo quando os heaps chegam a gigabytes ou terabytes.
# See exactly what the collector is doing and how long it pauses
java -Xlog:gc -XX:+UseG1GC MyApp
# Sample output line:
# [0.412s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 24M->5M(64M) 1.832msEssa linha de log vale ser decodificada: 24M->5M(64M) significa que o heap usado caiu de 24 MB para 5 MB de um total de 64 MB, e a aplicação ficou pausada por 1.832ms. Ler a saída de -Xlog:gc é a habilidade de ajuste de GC mais útil — meça antes de mudar qualquer flag.
Observando a coleta pelo código
Você não pode invocar diretamente um algoritmo específico pelo Java, mas pode observar a coleta acontecendo. Uma WeakReference permite manter um ponteiro que não mantém seu alvo vivo, então você pode perguntar "este objeto já foi coletado?" A classe Runtime relata o uso do heap, e System.gc() é uma sugestão — nunca um comando — para executar uma coleta agora.
import java.lang.ref.WeakReference;
WeakReference<byte[]> ref = new WeakReference<>(new byte[1024]);
System.out.println("Before GC: " + (ref.get() != null)); // true
System.gc();
System.out.println("After GC: " + (ref.get() != null)); // usually falseO exemplo executável abaixo reúne essas peças: ele aloca uma onda de lixo, mantém um sobrevivente alcançável, observa um objeto inacessível por meio de uma referência fraca e mede o heap antes e depois de uma coleta.
O que absorver da execução:
- O Passo 2 exibe
true: o objeto observado ainda é fortemente alcançável pela variávelgarbage, então aWeakReferenceconsegue lê-lo. - O Passo 3 relata o heap usado após 300.000 alocações de curta duração. O número exato varia de execução para execução — um GC menor pode já ter varrido grande parte dessa onda no meio do loop — mas criá-la é precisamente o churning de geração jovem que todo coletor geracional é construído para lidar de forma barata.
- O Passo 4 exibe
true, confirmando que o objeto observado foi recuperado assim quegarbage = nullo tornou inacessível eSystem.gc()acionou uma coleta — prova de que a perda de alcançabilidade, não sair do escopo, é o que libera memória. - O Passo 5 exibe
true: osurvivor, ainda referenciado por uma variável local viva (uma raiz de GC), atravessa a coleta intocado. - O Passo 6 mostra o heap usado caindo de volta próximo ao valor de referência, demonstrando que o coletor retorna o espaço recuperado para reutilização em vez de o programa vazar memória.
Para mais informações sobre os tipos de referência usados aqui — forte, suave, fraca e fantasma — veja Java References.