Coleta de Lixo em Java
Como funciona a coleta de lixo em Java: alcançabilidade, raízes GC, heap geracional, mark-sweep-compact, tipos de referência e coletores.
Em Java você nunca chama free(). A JVM rastreia cada objeto que você aloca no heap e, quando um objeto não pode mais ser alcançado pelo seu programa em execução, o coletor de lixo (GC) recupera sua memória automaticamente. Você escreve código que cria objetos; o GC limpa silenciosamente atrás de você. Entender como ele decide o que é lixo — e onde no heap ele procura — é a diferença entre código que escala e código que trava sob carga.
Esta página aborda como o GC decide o que manter (alcançabilidade), como o heap é organizado para uma coleta rápida, o algoritmo mark-sweep-compact, os quatro tipos de referência, como escolher um coletor e uma demonstração executável que torna a coleta observável.
Alcançabilidade e raízes GC
O GC não procura objetos com os quais você "terminou". Ele procura objetos que ainda são alcançáveis. Partindo de um conjunto de raízes GC, ele segue cada referência. Qualquer coisa que ele possa alcançar está viva; todo o resto é lixo, independentemente de você achar que ainda precisa disso.
| Raiz GC | Exemplo |
|---|---|
| Variáveis locais | Uma referência na pilha de uma thread em execução |
| Campos estáticos | static final Logger LOG = ... |
| Threads ativas | Um objeto Thread vivo |
| Referências JNI | Objetos mantidos por código nativo |
Definir uma referência como null (ou deixá-la sair do escopo) não exclui nada — apenas remove um caminho para o objeto. O objeto só se torna coletável quando nenhum caminho a partir de qualquer raiz permanece.
Object a = new Object(); // reachable via local variable 'a'
Object b = a; // now two references point to the same object
a = null; // still reachable through 'b' — not garbage
b = null; // now unreachable — eligible for collectionO heap geracional
A maioria dos objetos morre jovem — um escopo de requisição, uma variável temporária de loop, uma string intermediária. A JVM explora essa hipótese geracional fraca dividindo o heap em regiões e coletando a área jovem com muito mais frequência do que a antiga.
| Região | Contém | Coletada |
|---|---|---|
| Jovem (Eden + 2 espaços Survivor) | Objetos recém-alocados | Frequentemente, por um rápido minor GC |
| Antiga (Tenured) | Objetos que sobreviveram a vários minor GCs | Raramente, por um major/full GC mais lento |
| Metaspace | Metadados de classes (não seus objetos) | Quando classloaders são descarregados |
Novos objetos chegam no Eden. Um minor GC copia os poucos sobreviventes para um espaço Survivor; objetos que continuam sobrevivendo são eventualmente promovidos para a geração antiga. Como minor GCs apenas varrem a pequena região jovem, eles são baratos — e é por isso que a alocação de curta duração em Java é rápida. (Para saber como pilha e heap diferem, veja Stack vs Heap; para o papel da JVM que hospeda o heap, veja Arquitetura JVM.)
Marcar, varrer, compactar
Uma coleta é executada em fases. Primeiro ela marca cada objeto alcançável percorrendo o grafo a partir das raízes. Em seguida ela varre, liberando os objetos não marcados. Muitos coletores adicionam uma fase de compactação que desliza os objetos sobreviventes juntos para que o espaço livre seja um bloco contíguo — o que mantém a alocação como um simples incremento de ponteiro e evita a fragmentação.
// Pseudocode of what the collector does for you:
// 1. mark: visit(roots); for each reachable object, set live = true
// 2. sweep: for each object on the heap, if !live -> reclaim its memory
// 3. compact: move survivors next to each other, update referencesVocê pode sugerir uma coleta com System.gc(), mas é apenas uma dica — a JVM pode ignorá-la. Nunca dependa disso para corretude; trate-o como uma ferramenta de diagnóstico, não como uma estratégia de gerenciamento de memória.
Força das referências
Nem toda referência mantém um objeto vivo igualmente. O pacote java.lang.ref permite que você diga ao GC o quanto você quer que um objeto seja mantido, o que é a base dos caches sensíveis à memória.
| Referência | Comportamento do GC |
|---|---|
Forte (o = comum) | Nunca coletada enquanto alcançável |
SoftReference | Coletada somente quando a memória está baixa — boa para caches |
WeakReference | Coletada no próximo GC assim que nenhuma referência forte permanece |
PhantomReference | Usada para agendar limpeza após a coleta |
import java.lang.ref.WeakReference;
byte[] data = new byte[1024];
WeakReference<byte[]> ref = new WeakReference<>(data);
data = null; // drop the only strong reference
// After the next GC, ref.get() may return null.Vazamentos de memória ainda acontecem
Um coletor de lixo libera você de ponteiros pendentes e duplas liberações, mas não de vazamentos. Um vazamento de memória em Java é um objeto que você não usa mais, mas que ainda é alcançável a partir de uma raiz, então o GC deve mantê-lo. O heap fica cheio, o GC roda cada vez mais frequentemente e, eventualmente, você recebe OutOfMemoryError.
As causas clássicas são todas "esqueci de largar":
- Uma coleção
static(cache, lista de listeners, map) à qual você continua adicionando mas nunca remove. Campos estáticos são raízes GC, portanto tudo que eles alcançam vive para sempre. - Listeners ou callbacks registrados em um objeto de longa duração e nunca desregistrados.
- Chaves deixadas em um
HashMapmuito depois de serem necessárias, porque o map ainda as referencia.
A solução não é uma flag — é largar as referências quando terminar (remover da coleção, desregistrar o listener) ou usar uma estrutura baseada em WeakReference como WeakHashMap para que o GC possa recuperar entradas quando nada mais aponta para a chave.
finalize() é obsoleto e não confiável — a JVM pode executá-lo tarde ou nunca. Para liberar arquivos, sockets ou outros recursos que não são memória de forma determinística, use try-with-resources e AutoCloseable, não o coletor de lixo.Escolhendo um coletor
A JVM HotSpot vem com vários coletores com diferentes compensações entre throughput (trabalho total realizado) e latência (duração das pausas). Você escolhe um com uma flag da JVM; o padrão desde o Java 9 é o G1.
| Coletor | Flag | Melhor para |
|---|---|---|
| G1 (padrão) | -XX:+UseG1GC | Latência/throughput equilibrados, heaps grandes |
| Parallel | -XX:+UseParallelGC | Jobs em lote que priorizam throughput bruto |
| ZGC | -XX:+UseZGC | Heaps muito grandes, pausas sub-milissegundo |
| Serial | -XX:+UseSerialGC | Heaps pequenos, single-core ou contêineres |
# Pick a collector and set the heap size at launch:
java -XX:+UseG1GC -Xms256m -Xmx2g MyApp
# Print what the GC is doing, with timestamps:
java -Xlog:gc* MyAppUm exemplo prático
O programa abaixo torna o comportamento do GC observável. Ele fixa um objeto com uma referência forte, mantém outro apenas por meio de uma WeakReference, gera uma onda de lixo de curta duração, depois solicita uma coleta e reporta o que sobreviveu e como o uso do heap mudou.
O que observar na execução:
- O referente fraco imprime
trueantes da coleta efalsedepois dela, provando que umaWeakReferencenão mantém seu objeto vivo quando nenhuma referência forte permanece. - O array
keptmantido fortemente imprimesurvived: truemesmo apósSystem.gc(), porque é alcançável a partir de uma raiz GC e o coletor deve preservá-lo. - Aproximadamente 300 MB de lixo são alocados (
Bytes allocated as garbage: 307200000), mas o heap usado sobe apenas para cerca de 5 MB — os minor GCs recuperam os arrays de loop de curta duração tão rápido quanto são criados. Runtime.maxMemory()reporta o teto do heap (cerca de 256 MB aqui), definido por-Xmx, enquantototalMemory() - freeMemory()é a porção usada viva que fica próxima de 3–5 MB ao longo do tempo.System.gc()é apenas uma dica, mas nesta JVM ele é executado: o heap usado cai novamente e o referente fraco inacessível é limpo em vez de persistir.