W3docs

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ãoO que vive aquiCom que frequência é coletada
EdenObjetos recém-alocadosA cada GC menor
Survivor (S0/S1)Objetos que sobreviveram a um GC menorA cada GC menor
Old (tenured)Objetos de longa duração, promovidosGC maior / completo
MetaspaceMetadados 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 compaction

A tabela abaixo é o modelo mental a ser utilizado:

ColetorPonto forteComportamento de pausaUso típico
SerialO mais simples, baixo footprintStop-the-world, thread únicaHeaps pequenos, containers, CLIs
ParallelMaior throughputStop-the-world, múltiplas threadsBatch / processamento de dados
G1Equilibrado, previsívelMajoritariamente concorrente, pausa-alvoPadrão de uso geral
ZGCLatência muito baixaSub-milissegundo, concorrenteHeaps de vários GB a TB
ShenandoahLatência muito baixaPausas independentes do tamanho do heapServiç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.832ms

Essa 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 false

O 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.

java— editable, runs on the server

O que absorver da execução:

  • O Passo 2 exibe true: o objeto observado ainda é fortemente alcançável pela variável garbage, então a WeakReference consegue 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 que garbage = null o tornou inacessível e System.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: o survivor, 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.

Prática

Prática
Qual única propriedade determina se o coletor de lixo manterá um objeto, independentemente do algoritmo em uso?
Qual única propriedade determina se o coletor de lixo manterá um objeto, independentemente do algoritmo em uso?
Was this page helpful?