Vazamentos de Memória em Java: Causas e Como Detectar

Backend2026-09-27TryQuickToolBox

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:

  1. 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.
  2. 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.
  3. Capture um heap dump. Quando o heap estiver alto, execute jmap -dump:live,format=b,file=heap.hprof <pid> ou use jcmd <pid> GC.heap_dump heap.hprof. Você também pode disparar um dump em OutOfMemoryError com -XX:+HeapDumpOnOutOfMemoryError.
  4. 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.
  5. Faça um segundo dump e compare. Se os mesmos objetos crescerem entre os dumps, você encontrou o vazamento.
  6. 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:

FerramentaTipoMelhor Para
Eclipse MATAnalisador de heap dumpEncontrar dominadores e suspeitos de vazamento
VisualVMMonitoramento + heap dumpVerificações rápidas ao vivo e análise básica
JProfiler / YourKitProfilerRastreamento de alocação e cadeias de referência
async-profilerProfiler de baixo overheadProfiling 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:

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.