Vazamentos de Memória em Java: Causas e Como Detectar
Sua aplicação Java inicia rápido, mas ao longo de horas ou dias ela fica extremamente lenta, lança OutOfMemoryError ou é morta pelo container. Você a reinicia, e o ciclo se repete. Essa é a assinatura clássica de um vazamento de memória em Java: objetos que não são mais necessários mas permanecem acessíveis, então o garbage collector não consegue recuperá-los. Diferente de C/C++, o GC do Java lida com a maior parte da memória automaticamente, mas vazamentos ainda acontecem quando referências são mantidas sem intenção. Este artigo explica as causas mais comuns e oferece um fluxo de trabalho prático para detectá-las e corrigi-las.
O Que É um Vazamento de Memória em Java?
Um vazamento de memória ocorre quando objetos não são mais usados pela aplicação mas ainda são referenciados, impedindo a coleta de lixo. Com o tempo, o uso do heap cresce, o GC é executado com mais frequência e, eventualmente, a JVM lança OutOfMemoryError: Java heap space. Vazamentos podem ser pequenos e lentos (alguns KB por requisição) ou grandes e rápidos (armazenar conjuntos de resultados inteiros em cache).
Causas Comuns de Vazamentos de Memória em Java
1. Coleções Estáticas Que Crescem Infinitamente
Campos estáticos vivem por toda a vida da JVM. Um Map estático usado como cache sem eviction é um vazamento clássico. Por exemplo:
public class UserCache {
private static final Map<Long, User> CACHE = new HashMap<>();
public static void put(Long id, User user) {
CACHE.put(id, user); // nunca removido
}
}
Cada usuário adicionado permanece para sempre. Correção: use um cache limitado como Caffeine ou Guava com limites de tamanho e expiração.
2. Recursos Não Fechados
Streams, conexões e readers devem ser fechados. Se você esquecer, recursos nativos e seus wrappers Java vazam. Sempre use try-with-resources:
try (InputStream in = new FileInputStream(file)) {
// usa in
} // fechado automaticamente
Isso também se aplica a conexões de banco de dados, clientes HTTP e pools de threads.
3. Listeners e Callbacks Não Removidos
Quando você registra um listener (ex.: addListener) mas nunca o remove, o publicador mantém uma referência ao listener, que pode manter uma referência a todo o seu grafo de objetos. Isso é comum em GUIs, event buses e contextos de longa duração.
4. Variáveis ThreadLocal
Valores de ThreadLocal são armazenados no ThreadLocalMap da thread. Em pools de threads, as threads são reutilizadas, então se você não chamar remove(), o valor permanece ligado à thread e vaza. Sempre limpe:
try {
threadLocal.set(context);
// trabalho
} finally {
threadLocal.remove();
}
5. Classes Internas Mantendo Referências Externas
Classes internas não estáticas (incluindo classes anônimas) mantêm uma referência implícita à instância externa. Se uma instância de classe interna sobrevive ao objeto externo (ex.: armazenada em uma lista estática), o objeto externo não pode ser coletado. Torne classes internas estáticas quando não precisarem da instância externa.
6. equals() e hashCode() Inadequados
Se você usa objetos como chaves em um HashMap mas sobrescreve equals() sem hashCode() (ou vice-versa), as buscas falham e as entradas se acumulam. Sempre sobrescreva ambos de forma consistente.
7. String Interning e Substrings
Antes do Java 7, String.substring() compartilhava o array de char original, então uma pequena substring podia manter um array grande. O Java moderno copia, mas interning de muitas strings únicas (ex.: String.intern()) ainda pode encher o pool de strings.
Como Detectar Vazamentos de Memória em Java
A detecção segue um processo sistemático. Aqui está uma abordagem passo a passo:
- Monitore o uso do heap ao longo do tempo. Use JVisualVM, JConsole ou seu APM. Uma aplicação saudável mostra um padrão dente de serra: o heap cresce, o GC recupera, o heap cai. Um vazamento mostra uma linha de base crescente após cada GC.
- Habilite o log de GC. Adicione
-Xlog:gc*:file=gc.log:time,uptime,level,tags(Java 9+) ou-XX:+PrintGCDetails(Java 8). Analise os logs com ferramentas como GCeasy para ver se a old gen continua crescendo. - Capture um heap dump. Quando o heap estiver alto, execute
jmap -dump:live,format=b,file=heap.hprof <pid>ou usejcmd <pid> GC.heap_dump heap.hprof. Você também pode disparar um dump em OutOfMemoryError com-XX:+HeapDumpOnOutOfMemoryError. - Analise o heap dump. Abra-o no Eclipse MAT ou VisualVM. Procure a árvore de dominadores para encontrar objetos que retêm mais memória. Verifique coleções suspeitas com grandes tamanhos retidos.
- Faça um segundo dump e compare. Se os mesmos objetos crescerem entre os dumps, você encontrou o vazamento.
- Use um profiler para análise ao vivo. Ferramentas como YourKit, JProfiler ou async-profiler podem rastrear locais de alocação e cadeias de referência sem parar a aplicação.
Aqui está uma comparação rápida das ferramentas comuns:
| Ferramenta | Tipo | Melhor Para |
|---|---|---|
| Eclipse MAT | Analisador de heap dump | Encontrar dominadores e suspeitos de vazamento |
| VisualVM | Monitoramento + heap dump | Verificações rápidas ao vivo e análise básica |
| JProfiler / YourKit | Profiler | Rastreamento de alocação e cadeias de referência |
| async-profiler | Profiler de baixo overhead | Profiling em produção com flame graphs |
Corrigindo e Prevenindo Vazamentos
Depois de identificar o vazamento, corrija-o removendo a referência não intencional. Correções comuns:
- Substitua caches ilimitados por limitados (Caffeine, Ehcache).
- Use try-with-resources para todos os recursos Closeable.
- Desregistre listeners em um bloco finally ou use referências fracas.
- Sempre chame
ThreadLocal.remove(). - Torne classes internas estáticas quando possível.
- Sobrescreva equals() e hashCode() juntos.
Prevenir é melhor que remediar. Adicione monitoramento de memória ao seu pipeline de CI/CD, execute testes de carga com verificações de heap e defina -Xmx adequadamente. Use ferramentas de análise estática como SpotBugs para capturar padrões comuns.
FAQ
Qual é a diferença entre um vazamento de memória e um pico de memória?
Um pico de memória é um aumento temporário no uso do heap que o GC recupera. Um vazamento é um aumento constante ao longo do tempo porque os objetos permanecem acessíveis e não podem ser coletados.
Um vazamento de memória em Java pode causar OutOfMemoryError mesmo se o heap for grande?
Sim. Se a taxa de vazamento exceder a capacidade do GC de recuperar, o heap eventualmente encherá independentemente do tamanho. Aumentar o heap apenas atrasa a falha.
Como encontro um vazamento de memória em produção sem downtime?
Use ferramentas de baixo overhead como async-profiler ou JFR (Java Flight Recorder) para registrar alocações e estatísticas de heap. Você também pode disparar um heap dump com jcmd enquanto a aplicação roda, embora isso possa causar uma breve pausa.
Precisa analisar logs de GC ou outros logs baseados em texto? Experimente nosso Nginx Log Analyzer para analisar e visualizar padrões de log rapidamente.