1. Introdução ao coletor de lixo (GC)
Se você já programou em C ou C++, certamente teve de liberar memória manualmente com free() ou delete. Em Java, tudo é bem mais simples: você cria um objeto com new e não precisa removê-lo — isso é tarefa de um “zelador” especial chamado coletor de lixo (Garbage Collector, GC).
O GC é parte da JVM que libera automaticamente a memória ocupada por objetos que não possuem mais referências. Graças a ele, desenvolvedores Java não precisam se preocupar em esquecer de limpar a memória (e ter vazamento) ou, ao contrário, remover acidentalmente um objeto ainda necessário (e causar um crash).
Mas, como qualquer zelador, o GC não é perfeito: às vezes pode interferir no pior momento, fazendo uma “faxina geral” (Stop-the-World), ou não trabalhar tão rápido quanto gostaríamos. Por isso, a JVM oferece várias implementações de coletores de lixo — e escolher a correta pode impactar significativamente o desempenho do aplicativo.
Tipos principais de coletores de lixo
Serial GC
- Serial GC — o coletor de lixo mais simples e antigo.
- Trabalha em uma única thread.
- Interrompe todas as outras threads durante a coleta (Stop-the-World).
- Bom para aplicativos pequenos sem multithreading intenso.
- Ativado com o flag: -XX:+UseSerialGC
Parallel GC
- Parallel GC (também “Throughput Collector”).
- Usa múltiplas threads para a coleta de lixo.
- Focado em máxima vazão (throughput).
- Ainda realiza a coleta com pausas Stop-the-World, mas mais rápido que o Serial.
- Adequado para aplicativos de servidor em que pequenas pausas não são críticas.
- Ativado com o flag: -XX:+UseParallelGC
CMS (Concurrent Mark Sweep)
- CMS — GC obsoleto, mas por muito tempo popular, que minimiza pausas.
- Trabalha parcialmente em paralelo com o aplicativo, reduzindo o tempo de parada.
- Mais complexo de configurar e com sobrecarga adicional.
- Desde o Java 9 está marcado como obsoleto (deprecated).
- Ativado com o flag: -XX:+UseConcMarkSweepGC
G1 (Garbage First)
- G1 GC — coletor moderno padrão (a partir do Java 9).
- Equilibra pausas mínimas e desempenho.
- Divide o heap em muitas regiões (modelo por regiões).
- Pode coletar lixo seletivamente por regiões, sem tocar o heap inteiro.
- Permite definir uma pausa máxima alvo, por exemplo -XX:MaxGCPauseMillis=200.
- Flag de ativação: -XX:+UseG1GC (geralmente desnecessário, pois o G1 é o padrão).
ZGC e Shenandoah
- ZGC e Shenandoah — coletores de lixo modernos de baixa latência (low‑latency).
- Objetivo — pausas mínimas (milissegundos), mesmo em heaps enormes (até terabytes).
- Operam praticamente totalmente em paralelo com o aplicativo.
- Exigem Java 11+ (ZGC) ou Java 12+ (Shenandoah).
- Indicados para sistemas críticos de latência (bolsas, fintech, análise em tempo real).
- Flags de ativação: -XX:+UseZGC ou -XX:+UseShenandoahGC
3. Princípios de funcionamento dos GC modernos
Geração jovem e geração antiga (Young/Old Generation)
A JVM divide o heap em duas grandes partes:
Geração jovem (Young Generation): todos os novos objetos vão para cá. Aqui, a coleta acontece com frequência e rapidez ( Minor GC ).
Geração antiga (Old Generation, Tenured): para onde migram os objetos que “sobreviveram” a várias coletas na geração jovem. Aqui, a coleta é menos frequente, porém mais demorada ( Major/Full GC ).
Por quê? A maioria dos objetos em Java vive por muito pouco tempo (por exemplo, strings temporárias, coleções dentro de um método). Portanto, é possível coletar a geração jovem com rapidez e frequência, sem tocar a geração antiga.
Minor GC
- Limpa apenas a geração jovem.
- Rápido, com pausa curta.
- Não afeta os objetos antigos.
Major (Full) GC
- Limpa todo o heap (tanto a geração jovem quanto a antiga).
- Pode levar muito tempo (segundos ou mais em heaps grandes).
- Geralmente é acompanhado por uma pausa longa do aplicativo.
Como o GC decide quais objetos remover?
O GC procura objetos “vivos” a partir do conjunto de raízes (root set): variáveis locais nas pilhas das threads, campos estáticos, parâmetros de métodos etc. Tudo o que for alcançável é considerado vivo. O restante é lixo.
4. Comparação dos coletores modernos: G1, ZGC, Shenandoah
Vamos entender como se diferenciam os GC mais modernos e populares. Para isso, segue uma tabela ilustrativa:
| Coletor | Objetivo principal | Modelo de memória | Pausas mínimas | Escalabilidade | Suporte | Quando usar |
|---|---|---|---|---|---|---|
| G1 | Equilíbrio entre pausas e desempenho | Regiões | ~10–200 ms | Até centenas de GB | Java 9+ (padrão) | A maioria dos aplicativos de servidor |
| ZGC | Pausa mínima | Regiões, “marcas coloridas” | <10 ms | Até terabytes | Java 11+ | Tempo real, crítico de latência |
| Shenandoah | Pausa mínima | Regiões, “marcas coloridas” | <10 ms | Até terabytes | Java 12+ (Red Hat) | Tempo real, crítico de latência |
G1 GC: Garbage First
- Divide o heap em muitas regiões (geralmente 1–32 MB cada).
- Durante a coleta, escolhe as regiões com mais lixo (“garbage first”).
- Pode coletar apenas uma parte do heap, não tudo de uma vez.
- Permite definir a pausa alvo: -XX:MaxGCPauseMillis=200.
- Adequado para equilibrar desempenho e pausas; usado por padrão desde o Java 9.
Exemplo de ativação (se por acaso estiver desativado):
java -XX:+UseG1GC -jar myapp.jar
ZGC: Z Garbage Collector
- Experimental no Java 11, estável a partir do Java 15.
- Quase não para o aplicativo: as pausas geralmente são <10 ms, mesmo com 1–2 TB de heap.
- Usa “marcas coloridas” (coloring) e ponteiros especiais.
- Requer uma JVM de 64 bits; não funciona em sistemas de 32 bits.
- Suportado em Linux, macOS e Windows.
Exemplo de ativação:
java -XX:+UseZGC -jar myapp.jar
Shenandoah
- Desenvolvido pela Red Hat; objetivos semelhantes aos do ZGC.
- Pausas mínimas, trabalho paralelo ativo com o aplicativo.
- Suporte para Linux e Windows; parte de builds do OpenJDK.
- Usa técnicas semelhantes, mas algoritmos internos diferentes.
Exemplo de ativação:
java -XX:+UseShenandoahGC -jar myapp.jar
Comparação visual
graph TD
A[Geração jovem] -->|Minor GC| B[Geração antiga]
B -->|Major GC| C[Pausa do GC]
D[G1: regiões] --> E[Regiões selecionadas]
F[ZGC/Shenandoah: regiões] --> G[Coleta paralela]
5. Prática: como descobrir e alterar o GC
Como descobrir qual GC está sendo usado?
- Logs da JVM: Inicie o aplicativo com os parâmetros -Xlog:gc* (Java 9+) ou -verbose:gc (até o Java 8). Nos logs será possível ver qual GC está em uso e com que frequência ocorrem as pausas.
- jcmd: Execute:
onde <pid> é o identificador do processo Java.jcmd <pid> VM.flags - jvisualvm: Na seção “Monitoramento” é possível ver o tipo de GC.
Como alterar o GC do seu aplicativo?
Adicione o flag necessário ao iniciar o programa Java:
G1 GC (padrão, pode ser indicado explicitamente):
java -XX:+UseG1GC -jar myapp.jar
ZGC:
java -XX:+UseZGC -jar myapp.jar
Shenandoah:
java -XX:+UseShenandoahGC -jar myapp.jar
Como definir o tamanho do heap e as pausas?
- Tamanho máximo do heap: -Xmx2G
- Tamanho mínimo do heap: -Xms512M
- Para G1: pausa desejada — -XX:MaxGCPauseMillis=200
Exemplo de execução completa:
java -Xms512M -Xmx2G -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar myapp.jar
6. Particularidades da escolha do GC para diferentes tarefas
Quando escolher o G1
- Na maioria dos aplicativos de servidor e desktop — excelente escolha padrão.
- Funciona bem com heaps de centenas de megabytes a centenas de gigabytes.
- Equilibra desempenho e pausas.
Quando escolher ZGC ou Shenandoah
- Se o aplicativo for sensível à latência (crítico de latência: bolsas, jogos on-line, análise em tempo real).
- Se o heap for enorme (centenas de gigabytes ou mais).
- Se apenas pausas mínimas forem aceitáveis (milissegundos).
- Exigem Java 11+ (ZGC) ou Java 12+ (Shenandoah).
Quando o Parallel GC é suficiente
- Para aplicativos pequenos onde a vazão máxima é mais importante e as pausas não são críticas.
- Para processamento em lote (batch), onde é possível “aguentar” uma parada de Full GC.
7. Exemplo: comparando o comportamento de GC em um aplicativo simples
Um pequeno aplicativo que gera muitos objetos temporários (simulação de processamento de pedidos):
public class GCSimulator {
public static void main(String[] args) {
while (true) {
// Criamos 100.000 objetos a cada ciclo
for (int i = 0; i < 100_000; i++) {
String s = new String("Order-" + i);
}
// Damos uma pequena pausa
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}
}
Execute-o com diferentes GC e observe os logs:
java -Xmx256M -XX:+UseG1GC -Xlog:gc* GCSimulator
java -Xmx256M -XX:+UseZGC -Xlog:gc* GCSimulator
O que você verá?
O G1 fará pausas frequentes, porém curtas. ZGC/Shenandoah — pausas ainda mais curtas, mas podem ocorrer com maior frequência. Parallel GC — pausas mais longas, porém mais raras.
8. Erros comuns e nuances ao trabalhar com GC
Erro nº 1: Esperar que o GC resolva todos os problemas de memória. O GC não é uma varinha mágica. Se você mantiver referências para objetos desnecessários, nenhum GC vai ajudar — haverá vazamento de memória.
Erro nº 2: Chamar System.gc() explicitamente. A JVM sabe melhor quando coletar lixo. Forçar o GC pode causar uma pausa longa e reduzir o desempenho.
Erro nº 3: Ignorar os logs do GC. Se você não observar os logs do GC, pode não perceber que seu aplicativo “congela” regularmente em Full GC.
Erro nº 4: Usar GC obsoletos. Por exemplo, CMS não é mais desenvolvido. É melhor migrar para o G1 ou para coletores modernos de baixa latência.
Erro nº 5: Escolher o GC errado para a tarefa. Se você tem um aplicativo crítico de latência e usa o Parallel GC — espere pausas longas. Se for processamento em lote e você ativar o ZGC — terá sobrecarga desnecessária.
GO TO FULL VERSION