Corrija “java.lang.OutOfMemoryError: Java heap space” em Java: Causas, Conceitos Básicos de Heap e Soluções Práticas

目次

1. Introdução

Ao desenvolver em Java, você já teve sua aplicação falhar repentinamente e o console exibir:

java.lang.OutOfMemoryError: Java heap space

Esse erro significa “Java ficou sem memória utilizável (o heap).”
Entretanto, apenas pela mensagem de erro, não é imediatamente óbvio:

  • O que fez o heap esgotar
  • O que você deve ajustar, e como
  • Se o problema está no código ou na configuração

Como resultado, as pessoas costumam recorrer a “soluções rápidas” como “apenas aumentar -Xmx” ou “adicionar mais memória ao servidor.”

Mas aumentar o tamanho do heap sem entender a causa raiz não é apenas uma solução temporária—pode também gerar outros problemas.

  • GC (coleta de lixo) torna-se mais pesada e os tempos de resposta degradam
  • A memória total do servidor fica apertada e afeta outros processos
  • Um vazamento de memória real permanece, e o OutOfMemoryError ocorre novamente

Por isso, “java heap space” não é apenas “memória baixa.”
Você deve tratá-lo como um sinal de um problema composto envolvendo design da aplicação, implementação e configurações de infraestrutura.

1-1. Público‑Alvo

Este artigo é destinado a leitores que:

  • Entender os fundamentos de Java (classes, métodos, coleções, etc.)
  • Mas não compreende totalmente como a memória é gerenciada dentro da JVM
  • Já encontrou “java heap space” ou OutOfMemoryError em desenvolvimento/teste/produção—ou quer estar preparado
  • Executa Java em Docker/containers/cloud e sente um leve incômodo quanto às configurações de memória

Anos de experiência em Java não importam.
Se você quer “compreender corretamente o erro e aprender a isolar a causa por conta própria,” este guia pretende ser diretamente útil no trabalho real.

1-2. O Que Você Vai Aprender Neste Artigo

Neste artigo, explicamos o erro “java heap space” a partir do mecanismo, não apenas uma lista de correções.

Os tópicos principais incluem:

  • O que é o heap Java wp:list /wp:list

    • Como ele difere da pilha
    • Onde os objetos são alocados
    • Padrões comuns que causam “java heap space” wp:list /wp:list

    • Carregamento em massa de grandes volumes de dados

    • Coleções e caches que crescem excessivamente
    • Vazamentos de memória (código que mantém referências vivas)
    • Como verificar e aumentar o tamanho do heap wp:list /wp:list

    • Opções de linha de comando ( -Xms , -Xmx )

    • Configurações de IDE (Eclipse / IntelliJ, etc.)
    • Pontos de configuração de servidores de aplicação (Tomcat, etc.)
    • Técnicas de economia de memória no código wp:list /wp:list

    • Revisitar como você usa coleções

    • Armadilhas ao usar streams e lambdas
    • Estratégias de fragmentação para grandes volumes de dados
    • A relação entre GC e o heap wp:list /wp:list

    • Como o GC funciona basicamente

    • Como ler logs de GC em nível básico
    • Detectando vazamentos de memória e usando ferramentas wp:list /wp:list

    • Obtendo um heap dump

    • Começando a analisar usando VisualVM ou Eclipse MAT
    • Coisas a observar em ambientes de contêiner (Docker / Kubernetes) wp:list /wp:list

    • A relação entre contêineres e -Xmx

    • Limites de memória via cgroups e o OOM Killer

Na segunda metade do artigo, também respondemos perguntas comuns em formato de FAQ, como:

  • “Devo apenas aumentar o heap por enquanto?”
  • “Até onde posso aumentar o heap com segurança?”
  • “Como posso, de forma aproximada, saber se é um vazamento de memória?”

1-3. Como Ler Este Artigo

O erro “java heap space” é importante tanto para pessoas que:

  • Precisam corrigir um incidente de produção imediatamente
  • Querem prevenir problemas antes que ocorram

Se você precisa de uma correção imediata, pode avançar para seções práticas como:

  • Como mudar o tamanho do heap
  • Como verificar vazamentos de memória

Por outro lado, se você deseja uma compreensão aprofundada, leia na seguinte ordem:

  1. O básico: “O que é o heap Java”
  2. Causas típicas
  3. Em seguida, soluções e passos de ajuste

Esse fluxo ajudará você a entender claramente o mecanismo por trás do erro.

2. O Que É o Heap Java?

Para entender corretamente o erro “java heap space”, você primeiro precisa saber como o Java gerencia a memória.
Em Java, a memória é dividida em várias áreas por finalidade, e entre elas o heap desempenha um papel crítico como o espaço de memória para objetos.

2-1. Visão Geral das Áreas de Memória do Java

Aplicações Java rodam na JVM (Java Virtual Machine).
A JVM possui múltiplas áreas de memória para lidar com diferentes tipos de dados. As três mais comuns são:

■ Tipos de Áreas de Memória

  • Heap – A área onde os objetos criados pela aplicação são armazenados. Se ela se esgotar, você recebe o erro “java heap space”.
  • Stack – A área para chamadas de método, variáveis locais, referências e mais. Se houver overflow, ocorre o “StackOverflowError”.
  • Method Area / Metaspace – Armazena informações de classes, constantes, metadados e resultados de compilação JIT.

Em Java, todos os objetos criados com new são colocados no heap.

2-2. O Papel do Heap

O heap do Java é onde são armazenados itens como os seguintes:

  • Objetos criados com new
  • Arrays (incluindo o conteúdo de List/Map, etc.)
  • Objetos gerados internamente por lambdas
  • Strings e buffers usados por StringBuilder
  • Estruturas de dados usadas dentro do framework de coleções

Em outras palavras, quando o Java precisa “manter algo na memória”, quase sempre isso é armazenado no heap.

2-3. O Que Acontece Quando o Heap Se Esgota?

Se o heap for muito pequeno — ou a aplicação criar objetos demais — o Java executa GC (coleta de lixo) para recuperar memória removendo objetos não utilizados.

Mas se o GC repetido ainda não liberar memória suficiente, e a JVM não puder mais alocar memória, você receberá:

java.lang.OutOfMemoryError: Java heap space

e a aplicação será forçada a parar.

2-4. “Apenas Aumente o Heap” É Meio Certo e Meio Errado

Se o heap está simplesmente pequeno demais, aumentá‑lo pode resolver o problema — por exemplo:

-Xms1024m -Xmx2048m

Entretanto, se a causa raiz for um vazamento de memória ou processamento ineficiente de grandes volumes de dados no código, aumentar o heap apenas compra tempo e não corrige o problema subjacente.

Em resumo, entender “por que o heap está se esgotando” é a coisa mais importante.

2-5. Layout do Heap (Eden / Survivor / Old)

O heap do Java é amplamente dividido em duas partes:

  • Geração jovem (objetos recém‑criados) wp:list /wp:list

    • Eden
    • Survivor (S0, S1)
    • Geração antiga (objetos de longa vida)

O GC funciona de maneira diferente dependendo da área.

Geração jovem

Objetos são primeiro colocados em Eden, e objetos de curta vida são rapidamente removidos.
O GC roda com frequência aqui, mas é relativamente leve.

Geração antiga

Objetos que sobrevivem tempo suficiente são promovidos de Young para Old.
O GC em Old é mais caro, portanto, se essa área continuar crescendo, pode causar latência ou pausas.

Na maioria dos casos, um erro “heap space” ocorre porque a geração Old se enche.

2-6. Por Que a Falta de Heap É Comum para Iniciantes e Desenvolvedores Intermediários

Como o Java realiza a coleta de lixo automaticamente, as pessoas costumam assumir que “a JVM cuida de todo o gerenciamento de memória”.

Na realidade, há muitas maneiras de esgotar o heap, como por exemplo:

  • Código que continua criando grande quantidade de objetos
  • Referências mantidas vivas dentro de coleções
  • Streams/lambdas que geram involuntariamente grandes volumes de dados
  • Caches excessivamente crescidos
  • Má compreensão dos limites de heap em contêineres Docker
  • Configuração incorreta de heap em uma IDE

É por isso que aprender como o próprio heap funciona é o caminho mais curto para uma correção confiável.

3. Causas Comuns do Erro “java heap space”

A escassez de heap é um problema frequente em muitos ambientes reais, mas suas causas podem ser agrupadas em três categorias principais: volume de dados, código/design e configuração inadequada.
Nesta seção, organizamos padrões típicos e explicamos por que eles levam ao erro.

3-1. Pressão de Memória ao Carregar Dados Grandes

O padrão mais comum é quando os próprios dados são tão grandes que o heap se esgota.

■ Exemplos Comuns

  • Carregando um enorme CSV/JSON/XML tudo de uma vez na memória
  • Buscando um número massivo de registros de banco de dados de uma só vez
  • Uma Web API retorna uma resposta muito grande (imagens, logs, etc.)

Um cenário particularmente perigoso é:

Quando a “string bruta antes da análise” e os “objetos após a análise” existem na memória ao mesmo tempo.

Por exemplo, se você carregar um JSON de 500MB como uma única string e depois deserializá-lo com Jackson, o uso total de memória pode facilmente exceder 1GB.

■ Direção para Mitigação

  • Introduza leitura em chunks (processamento de streaming)
  • Use paginação para acesso ao banco de dados
  • Evite manter dados intermediários por mais tempo do que o necessário

Seguir a regra “manipule dados grandes em chunks” vai longe na prevenção do esgotamento do heap.

3-2. Acumulando Excessivamente Dados em Coleções

Isso é extremamente comum para desenvolvedores de iniciante a intermediário.

■ Erros Típicos

  • Adicionando continuamente logs ou dados temporários a uma Listela cresce sem ser limpa
  • Usando um Map como cache (mas nunca removendo entradas)
  • Criando novos objetos continuamente dentro de loops
  • Gerando números enormes de objetos temporários via Streams ou lambdas

No Java, enquanto uma referência permanecer, o GC não pode remover o objeto.
Em muitos casos, os desenvolvedores mantêm referências vivas sem intenção.

■ Direção para Mitigação

  • Defina um ciclo de vida para caches
  • Defina limites de capacidade para coleções
  • Para mecanismos de dados grandes, limpe periodicamente

Para referência, mesmo que não “pareça” um vazamento de memória:

List<String> list = new ArrayList<>();
for (...) {
    list.add(heavyData);  // ← grows forever
}

Esse tipo de código é muito perigoso.

3-3. Vazamentos de Memória (Retenção Não Intencional de Objetos)

Porque o Java tem GC, as pessoas frequentemente pensam “vazamentos de memória não acontecem no Java.”
Na prática, vazamentos de memória absolutamente acontecem no Java.

■ Pontos Quentes Comuns de Vazamento

  • Mantendo objetos em variáveis estáticas
  • Esquecendo de desregistrar listeners ou callbacks
  • Deixando referências vivas dentro de Streams/Lambdas
  • Objetos acumulando em jobs de batch de longa execução
  • Armazenando dados grandes em ThreadLocal e a thread é reutilizada

Vazamentos de memória não são algo que você possa evitar completamente no Java.

■ Direção para Mitigação

  • Revise como você usa variáveis estáticas
  • Garanta que removeListener() e close() sejam sempre chamados
  • Para processos de longa execução, faça um dump do heap e investigue
  • Evite ThreadLocal a menos que realmente necessário

Porque vazamentos de memória vão recorrer mesmo se você aumentar o heap,
investigação da causa raiz é essencial.

3-4. Tamanho do Heap da JVM Muito Pequeno (Padrões São Pequenos)

Às vezes a aplicação está bem, mas o heap em si é simplesmente muito pequeno.

O tamanho padrão do heap varia por OS e versão do Java.
No Java 8, é comumente definido para aproximadamente 1/64 a 1/4 da memória física.

Uma configuração perigosa frequentemente vista em produção é:

No -Xmx specified, while the app processes large data

■ Cenários Comuns

  • Apenas produção tem volume de dados maior, e o heap padrão não é suficiente
  • Executando no Docker sem definir -Xmx
  • Spring Boot iniciado como um fat JAR com valores padrão

■ Direção para Mitigação

  • Defina -Xms e -Xmx para valores apropriados
  • Em contêineres, entenda memória física vs limites cgroup e configure de acordo

3-5. Padrões de Longa Execução Onde Objetos Continuam Acumulando

Aplicações como as seguintes tendem a acumular pressão de memória ao longo do tempo:

  • Aplicações Spring Boot de longa execução
  • Jobs de batch intensivos em memória
  • Aplicações web com grande tráfego de usuários

Jobs de batch em particular frequentemente mostram este padrão:

  • Memória é consumida
  • GC mal recupera o suficiente
  • Algum acúmulo permanece, e a próxima execução atinge OOM

Isso leva a muitos erros de espaço de heap de início tardio.

3-6. Má interpretação dos limites em contêineres (Docker / Kubernetes)

Existe uma armadilha comum no Docker/Kubernetes:

■ Armadilha

  • Não definir -Xmx → o Java referencia a memória física do host em vez do limite do contêiner → usa demais → o processo é finalizado pelo OOM Killer

Isso é um dos incidentes de produção mais comuns.

■ Mitigação

  • Defina -XX:MaxRAMPercentage adequadamente
  • Alinhe -Xmx com o limite de memória do contêiner
  • Entenda “UseContainerSupport” no Java 11+

4. Como verificar o tamanho do heap

Quando você vê um erro “java heap space”, a primeira coisa que deve fazer é confirmar quanto heap está atualmente alocado.
Na maioria dos casos, o heap simplesmente está menor do que o esperado — portanto, verificar é um passo crítico inicial.

Nesta seção, abordamos maneiras de verificar o tamanho do heap a partir da linha de comando, dentro do programa, IDEs e servidores de aplicação.

4-1. Verificar o tamanho do heap a partir da linha de comando

O Java fornece várias opções para verificar valores de configuração da JVM na inicialização.

■ Usando -XX:+PrintFlagsFinal

Esta é a maneira mais confiável de confirmar o tamanho do heap:

java -XX:+PrintFlagsFinal -version | grep HeapSize

Você verá uma saída como:

  • InitialHeapSize … o tamanho inicial do heap especificado por -Xms
  • MaxHeapSize … o tamanho máximo do heap especificado por -Xmx

Exemplo:

uintx InitialHeapSize                          = 268435456
uintx MaxHeapSize                              = 4294967296

Isso significa:

  • Heap inicial: 256 MB
  • Heap máximo: 4 GB

■ Exemplo concreto

java -Xms512m -Xmx2g -XX:+PrintFlagsFinal -version | grep HeapSize

Isso também é útil após alterar as configurações, tornando‑o um método de confirmação confiável.

4-2. Verificar o tamanho do heap a partir de um programa em execução

Às vezes você quer verificar a quantidade de heap de dentro da aplicação em execução.

O Java facilita isso usando a classe Runtime:

long max = Runtime.getRuntime().maxMemory();
long total = Runtime.getRuntime().totalMemory();
long free = Runtime.getRuntime().freeMemory();

System.out.println("Max Heap:    " + (max / 1024 / 1024) + " MB");
System.out.println("Total Heap:  " + (total / 1024 / 1024) + " MB");
System.out.println("Free Heap:   " + (free / 1024 / 1024) + " MB");
  • maxMemory() … o tamanho máximo do heap (-Xmx)
  • totalMemory() … o heap atualmente alocado pela JVM
  • freeMemory() … o espaço atualmente disponível dentro desse heap

Para aplicativos web ou processos de longa duração, registrar esses valores pode ajudar durante a investigação de incidentes.

4-3. Verificar usando ferramentas como VisualVM ou Mission Control

Você também pode inspecionar visualmente o uso do heap com ferramentas GUI.

■ VisualVM

  • Exibição em tempo real do uso do heap
  • Tempo de GC
  • Captura de dump do heap

É uma ferramenta clássica, comumente usada no desenvolvimento Java.

■ Java Mission Control (JMC)

  • Permite um profiling mais detalhado
  • Especialmente útil para operações no Java 11+

Essas ferramentas ajudam a visualizar problemas como apenas a geração Old crescendo.

4-4. Verificar em IDEs (Eclipse / IntelliJ)

Se você executar seu aplicativo a partir de uma IDE, as configurações da IDE podem afetar o tamanho do heap.

■ Eclipse

Window → Preferences → Java → Installed JREs

Ou defina -Xms / -Xmx em:
Configuração de Execução → Argumentos da VM

■ IntelliJ IDEA

Help → Change Memory Settings

Ou adicione -Xmx nas opções de VM na Configuração de Execução/Depuração.

Tenha cuidado — às vezes a própria IDE impõe um limite de heap.

4-5. Verificar em servidores de aplicação (Tomcat / Jetty)

Para aplicações web, o tamanho do heap costuma ser especificado nos scripts de inicialização do servidor.

■ Exemplo de Tomcat (Linux)

CATALINA_OPTS="-Xms512m -Xmx2g"

■ Exemplo de Tomcat (Windows)

set JAVA_OPTS=-Xms512m -Xmx2g

Na produção, deixar isso nos padrões é comum — e frequentemente leva a erros de espaço de heap depois que o serviço está em execução por um tempo.

4-6. Verificando o Heap no Docker / Kubernetes (Importante)

Em contêineres, memória física, cgroups e configurações do Java interagem de maneira complicada.

No Java 11+, “UseContainerSupport” pode ajustar o heap automaticamente, mas o comportamento ainda pode ser inesperado dependendo de:

  • O limite de memória do contêiner (ex.: --memory=512m )
  • Se -Xmx está definido explicitamente

Por exemplo, se você definir apenas um limite de memória para o contêiner:

docker run --memory=512m ...

e não definir -Xmx, você pode encontrar:

  • O Java referencia a memória do host e tenta alocar demais
  • Os cgroups aplicam o limite
  • O processo é finalizado pelo OOM Killer

Este é um problema muito comum em produção.

4-7. Resumo: Verificar o Heap é o Primeiro Passo Obrigatório

Escassez de heap requer correções muito diferentes dependendo da causa. Comece entendendo, como um conjunto:

  • O tamanho atual do heap
  • O uso real
  • Visualização via ferramentas

5. Solução #1: Aumentar o Tamanho do Heap

A resposta mais direta a um erro de “java heap space” é aumentar o tamanho do heap. Se a causa for simples escassez de memória, aumentar o heap adequadamente pode restaurar o comportamento normal.

Entretanto, ao aumentar o heap, é importante entender tanto os métodos corretos de configuração quanto as precauções principais. Configurações incorretas podem levar à degradação de desempenho ou a outros problemas de OOM (Out Of Memory).

5-1. Aumentar o Tamanho do Heap pela Linha de Comando

Se você iniciar uma aplicação Java como JAR, o método mais básico é especificar -Xms e -Xmx:

■ Exemplo: Inicial 512MB, Máx 2GB

java -Xms512m -Xmx2g -jar app.jar
  • -Xms … o tamanho inicial do heap reservado na inicialização da JVM
  • -Xmx … o tamanho máximo do heap que a JVM pode usar

Em muitos casos, definir -Xms e -Xmx com o mesmo valor ajuda a reduzir a sobrecarga de redimensionamento do heap.

Exemplo:

java -Xms2g -Xmx2g -jar app.jar

5-2. Configuração para Aplicações de Servidor Residentes (Tomcat / Jetty, etc.)

Para aplicações web, defina essas opções nos scripts de inicialização do servidor de aplicação.

■ Tomcat (Linux)

Defina em setenv.sh:

export CATALINA_OPTS="$CATALINA_OPTS -Xms512m -Xmx2048m"

■ Tomcat (Windows)

Defina em setenv.bat:

set CATALINA_OPTS=-Xms512m -Xmx2048m

■ Jetty

Adicione o seguinte a start.ini ou jetty.conf:

--exec
-Xms512m
-Xmx2048m

Como aplicativos web podem ter picos de uso de memória dependendo do tráfego, a produção deve geralmente ter mais margem de segurança do que os ambientes de teste.

5-3. Configurações de Heap para Aplicações Spring Boot

Se você executar Spring Boot como um fat JAR, o básico é o mesmo:

java -Xms1g -Xmx2g -jar spring-app.jar

Spring Boot tende a usar mais memória que um programa Java simples porque carrega muitas classes e configurações na inicialização.

Ele costuma consumir mais memória do que uma aplicação Java típica.

5-4. Configurações de Heap no Docker / Kubernetes (Importante)

Para Java em contêineres, você deve ter cuidado porque os limites do contêiner e o cálculo do heap da JVM interagem.

■ Exemplo Recomendado (Docker)

docker run --memory=1g \
  -e JAVA_OPTS="-Xms512m -Xmx800m" \
  my-java-app

■ Por Que Você Deve Definir Explicitamente -Xmx

Se você não especificar -Xmx no Docker:

  • A JVM decide o tamanho do heap com base na memória física da máquina host, não no contêiner
  • Pode tentar alocar mais memória do que o contêiner permite
  • Atinge o limite de memória do cgroup e o processo é finalizado pelo OOM Killer

Como este é um problema muito comum em produção, você deve sempre definir -Xmx em ambientes de contêiner.

5-5. Exemplos de Configuração de Heap para Ambientes CI/CD e Cloud

Em ambientes Java baseados em nuvem, uma regra prática comum é definir o heap com base na memória disponível:

Total MemoryRecommended Heap (Approx.)
1GB512–800MB
2GB1.2–1.6GB
4GB2–3GB
8GB4–6GB

※ Deixe a memória restante para o SO, sobrecarga de GC e pilhas de threads.

Em ambientes de nuvem, a memória total pode ser limitada. Se você aumentar o heap sem planejamento, toda a aplicação pode se tornar instável.

5-6. Aumentar o Heap Sempre Resolve? → Existem Limites

Aumentar o tamanho do heap pode eliminar temporariamente o erro, mas não resolve casos como:

  • Existe um vazamento de memória
  • Uma coleção continua crescendo indefinidamente
  • Dados enormes são processados em massa
  • O app tem um design incorreto

Então, trate o aumento do heap como uma medida de emergência, e certifique-se de seguir com otimização de código e revisão do design de processamento de dados, que cobriremos em seguida.

6. Solução #2: Otimize Seu Código

Aumentar o tamanho do heap pode ser uma mitigação eficaz, mas se a causa raiz estiver na estrutura do seu código ou na forma como você processa dados, o erro de “java heap space” retornará mais cedo ou mais tarde.

Nesta seção, cobriremos padrões de codificação comuns do mundo real que desperdiciam memória, e abordagens concretas para melhorá-los.

6-1. Repense Como Você Usa Coleções

As coleções Java (List, Map, Set, etc.) são convenientes, mas o uso descuidado pode facilmente se tornar a causa principal do crescimento de memória.

■ Padrão ①: List / Map Cresce Sem Limites

Um exemplo comum:

List<String> logs = new ArrayList<>();

while (true) {
    logs.add(fetchLog());   // ← grows forever
}

Coleções com nenhuma condição de término clara ou limite superior espremerão o heap de forma confiável em ambientes de longa execução.

● Melhorias
  • Use uma coleção limitada (ex.: limite o tamanho e descarte entradas antigas)
  • Limpe periodicamente valores que você não precisa mais
  • Se você usar um Map como cache, adote um cache com evicção → Guava Cache ou Caffeine são boas opções

■ Padrão ②: Não Definir Capacidade Inicial

ArrayList e HashMap crescem automaticamente quando excedem a capacidade, mas esse crescimento envolve:
alocando um novo array → copiando → descartando o array antigo.

Ao lidar com grandes conjuntos de dados, omitir a capacidade inicial é ineficiente e pode desperdiçar memória.

● Exemplo de Melhoria
List<String> items = new ArrayList<>(10000);

Se você puder estimar o tamanho, é melhor defini-lo antecipadamente.

6-2. Evite Processamento em Massa de Grandes Dados (Processe em Pedços)

Se você processar dados massivos de uma só vez, é fácil cair no pior cenário:
tudo acaba no heap → OOM.

■ Exemplo Ruim (Ler um Arquivo Enorme de Uma Só Vez)

String json = Files.readString(Paths.get("large.json"));
Object data = new ObjectMapper().readValue(json, Data.class);

■ Melhorias

  • Use processamento em streaming (ex.: Jackson Streaming API)
  • Leia em porções menores (paginação em lotes)
  • Processe streams sequencialmente e não retenha o conjunto de dados inteiro
● Exemplo: Processe JSON Enorme com Jackson Streaming
JsonFactory factory = new JsonFactory();
try (JsonParser parser = factory.createParser(new File("large.json"))) {
    while (!parser.isClosed()) {
        JsonToken token = parser.nextToken();
        // Perform only what you need, and do not retain it in memory
    }
}

6-3. Evite Criação Desnecessária de Objetos

Streams e lambdas são convenientes, mas eles podem gerar grandes números de objetos temporários internamente.

■ Exemplo Ruim (Criando uma Lista Intermediária Enorme com Streams)

List<Result> results = items.stream()
        .map(this::toResult)
        .collect(Collectors.toList());

Se items for enorme, um grande número de objetos temporários é criado, e o heap incha.

● Melhorias
  • Processe sequencialmente com um loop for
  • Escreva apenas o que você precisa imediatamente (não mantenha tudo)
  • Evite collect() , ou controle manualmente

6-4. Tenha Cuidado com Concatenação de Strings

As Strings Java são imutáveis, então cada concatenação cria um novo objeto.

■ Melhorias

  • Use StringBuilder para concatenação pesada
  • Evite concatenação desnecessária ao gerar logs
    StringBuilder sb = new StringBuilder();
    for (String s : items) {
        sb.append(s);
    }
    

6-5. Não Exagere na Construção de Caches

Esta é uma situação comum em aplicativos web e processamento em lote:

  • “Adicionamos um cache para velocidade.”
  • → mas esquecemos de limpá-lo
  • → o cache continua crescendo
  • → escassez de heap → OOM

■ Melhorias

  • Defina TTL (expiração baseada em tempo) e um tamanho máximo
  • Usar ConcurrentHashMap como substituto de cache é arriscado
  • Use um cache bem gerenciado como o Caffeine que controla a memória adequadamente

6-6. Não Recrie Objetos Dentro de Loops Grandes

■ Exemplo Ruim

for (...) {
    StringBuilder sb = new StringBuilder(); // created every iteration
    ...
}

Isso cria mais objetos temporários do que o necessário.

● Melhoria
StringBuilder sb = new StringBuilder(); 
for (...) {
    sb.setLength(0);  // reuse
}

6-7. Divida Trabalhos Pesados em Memória em Processos Separados

Quando você lida com dados verdadeiramente massivos em Java, pode precisar rever a arquitetura em si.

  • Separe ETL em um job de lote dedicado
  • Delegue para processamento distribuído (Spark ou Hadoop)
  • Divida serviços para evitar contenção de heap

6-8. Otimização de Código É um Passo Chave para Prevenir Recorrência

Se você apenas aumentar o heap, eventualmente atingirá o próximo “limite”, e o mesmo erro ocorrerá novamente.

Para prevenir fundamentalmente erros de “java heap space”, você deve:

  • Entender o volume de seus dados
  • Revisar padrões de criação de objetos
  • Melhorar o design de coleções

7. Solução #3: Ajuste GC (Garbage Collection)

O erro “java heap space” pode acontecer não apenas quando o heap é muito pequeno, mas também quando o GC não consegue recuperar a memória de forma eficaz e o heap gradualmente se torna saturado.

Sem entender o GC, você pode facilmente diagnosticar mal sintomas como:
“A memória deveria estar disponível, mas ainda recebemos erros”, ou “O sistema fica extremamente lento.”

Esta seção explica o mecanismo básico de GC no Java e pontos de ajuste práticos que ajudam em operações reais.

7-1. O Que É GC (Garbage Collection)?

O GC é o mecanismo do Java para descartar automaticamente objetos que não são mais necessários.
O heap do Java é amplamente dividido em duas gerações, e o GC se comporta de forma diferente em cada uma.

● Geração Jovem (objetos de curta duração)

  • Eden / Survivor (S0, S1)
  • Dados temporários criados localmente, etc.
  • O GC acontece frequentemente, mas é leve

● Geração Velha (objetos de longa duração)

  • Objetos promovidos da Jovem
  • O GC é mais pesado; se acontecer frequentemente, o app pode “congelar”

Em muitos casos, “java heap space” acontece ultimamente quando a Geração Velha se enche.

7-2. Tipos e Características do GC (Como Escolher)

O Java fornece múltiplos algoritmos de GC.
Escolher o certo para sua carga de trabalho pode melhorar significativamente o desempenho.

● ① G1GC (Padrão desde Java 9)

  • Divide o heap em pequenas regiões e as recupera incrementalmente
  • Pode manter pausas stop-the-world mais curtas
  • Ótimo para apps web e sistemas de negócios

→ Em geral, G1GC é uma escolha padrão segura

● ② Parallel GC (Bom para jobs de lote pesados em throughput)

  • Paralelizado e rápido
  • Mas tempos de pausa podem se tornar mais longos
  • Frequentemente benéfico para processamento em lote pesado em CPU

● ③ ZGC (GC de baixa latência com pausas em nível de milissegundo)

  • Disponível no Java 11+
  • Para apps sensíveis à latência (servidores de jogos, HFT)
  • Eficaz mesmo com heaps grandes (dezenas de GB)

● ④ Shenandoah (GC de baixa latência)

  • Frequentemente associado a distribuições Red Hat
  • Pode minimizar tempos de pausa agressivamente
  • Também disponível em algumas builds como AWS Corretto

7-3. Como Alternar Explicitamente o GC

O G1GC é o padrão em muitas configurações, mas você pode especificar um algoritmo de GC dependendo do seu objetivo:

# G1GC
java -XX:+UseG1GC -jar app.jar

# Parallel GC
java -XX:+UseParallelGC -jar app.jar

# ZGC
java -XX:+UseZGC -jar app.jar

Porque o algoritmo de GC pode mudar drasticamente o comportamento do heap e o tempo de pausa, sistemas de produção frequentemente definem isso explicitamente.

7-4. Saída de Logs de GC e Inspeção Visual de Problemas

É crucial entender quanto de memória o GC está recuperando e com que frequência as pausas stop-the-world ocorrem.

● Configuração Básica de Log de GC

java \
  -Xms1g -Xmx1g \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -Xloggc:gc.log \
  -jar app.jar

Ao examinar gc.log, você pode identificar sinais claros de pressão no heap, como:

  • Muitos Young GCs
  • A geração Old nunca diminui
  • Full GC ocorre frequentemente
  • Cada GC recupera uma quantidade incomumente pequena

7-5. Casos em que a Latência do GC Aciona “java heap space”

Se a pressão no heap for causada por padrões como os seguintes, o comportamento do GC se torna uma pista decisiva.

● Sintomas

  • O aplicativo congela repentinamente
  • GC executa por segundos a dezenas de segundos
  • A geração Old continua crescendo
  • Full GC aumenta, e finalmente OOM ocorre

Isso indica um estado em que o GC está se esforçando, mas não consegue recuperar memória suficiente antes de atingir o limite.

■ Causas Raiz Comuns

  • Vazamentos de memória
  • Coleções retidas permanentemente
  • Objetos vivendo por muito tempo
  • Inchaço da geração Old

Nesses casos, analisar os logs de GC pode ajudá-lo a identificar sinais de vazamento ou picos de carga em momentos específicos.

7-6. Pontos Chave ao Ajustar G1GC

O G1GC é forte por padrão, mas o ajuste pode torná-lo ainda mais estável.

● Parâmetros Comuns

-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=8m
-XX:InitiatingHeapOccupancyPercent=45
  • MaxGCPauseMillis → Tempo de pausa alvo (ex.: 200ms)
  • G1HeapRegionSize → Tamanho da região usada para particionar o heap
  • InitiatingHeapOccupancyPercent → Percentual de ocupação da geração Old que aciona um ciclo de GC

No entanto, na maioria dos casos os padrões são adequados, então altere apenas quando houver uma necessidade clara.

7-7. Resumo do Ajuste de GC

As melhorias no GC ajudam você a visualizar fatores que não são óbvios apenas aumentando o tamanho do heap:

  • Tempos de vida dos objetos
  • Padrões de uso de coleções
  • Se existe um vazamento de memória
  • Onde a pressão no heap se concentra

É por isso que o ajuste de GC é um processo altamente importante para mitigar “java heap space”.

8. Solução #4: Detectar Vazamentos de Memória

Se o erro ainda recorre mesmo após aumentar o heap e otimizar o código, o suspeito mais provável é um vazamento de memória.

As pessoas frequentemente assumem que o Java é resistente a vazamentos de memória porque o GC existe, mas na prática, vazamentos de memória são uma das causas mais problemáticas e propensas a recorrência em ambientes reais.

Aqui, focamos em etapas práticas que você pode usar imediatamente, desde entender vazamentos até usar ferramentas de análise como VisualVM e Eclipse MAT.

8-1. O Que É um Vazamento de Memória? (Sim, Acontece no Java)

Um vazamento de memória no Java é:

Um estado em que referências a objetos desnecessários permanecem, impedindo o GC de recuperá-los.

Mesmo com coleta de lixo, vazamentos ocorrem comumente quando:

  • Objetos são mantidos em campos static
  • Ouvintes registrados dinamicamente nunca são desregistrados
  • Coleções continuam crescendo e retêm referências
  • Valores ThreadLocal persistem inesperadamente
  • Ciclos de vida de framework não correspondem ao ciclo de vida do seu objeto

Então vazamentos são absolutamente uma possibilidade normal.

8-2. Padrões Típicos de Vazamento de Memória

● ① Crescimento de Coleção (Mais Comum)

Adicionando continuamente a List/Map/Set sem remover entradas.
Em sistemas Java de negócios, uma grande porção de incidentes OOM vem deste padrão.

● ② Mantendo Objetos em Variáveis static

private static List&lt;User&gt; cache = new ArrayList&lt;&gt;();

Isso frequentemente se torna o ponto de partida de um vazamento.

● ③ Esquecer de Desregistrar Ouvintes / Callbacks

Referências permanecem em segundo plano via GUI, observadores, ouvintes de eventos, etc.

● ④ Uso Incorreto de ThreadLocal

Em ambientes de thread-pool, valores ThreadLocal podem persistir por mais tempo do que o pretendido.

● ⑤ Referências Retidas por Bibliotecas Externas

Alguma “memória oculta” é difícil de gerenciar a partir do código da aplicação, tornando a análise baseada em ferramentas essencial.

8-3. Pontos de Verificação para Detectar “Sinais” de um Vazamento de Memória

Se você observar o seguinte, deve suspeitar fortemente de um vazamento de memória:

  • Apenas a geração Old aumenta continuamente
  • GC completo torna‑se mais frequente
  • A memória mal diminui mesmo após GC completo
  • O uso do heap aumenta com o tempo de atividade
  • Somente a produção falha após longos tempos de execução

Esses sinais são muito mais fáceis de entender quando visualizados com ferramentas.

8-4. Ferramenta #1: Verificar Vazamentos Visualmente com VisualVM

O VisualVM costuma ser incluído no JDK em algumas configurações e é muito acessível como primeira ferramenta.

● O Que Você Pode Fazer com o VisualVM

  • Monitoramento em tempo real do uso de memória
  • Confirmar o crescimento da geração Old
  • Frequência de GC
  • Monitoramento de threads
  • Capturar dumps de heap

● Como Capturar um Dump de Heap

No VisualVM, abra a aba “Monitor” e clique no botão “Heap Dump”.

Você pode então passar o dump de heap capturado diretamente para o Eclipse MAT para uma análise mais profunda.

8-5. Ferramenta #2: Análise Profunda com Eclipse MAT (Memory Analyzer Tool)

Se existe uma ferramenta padrão da indústria para análise de vazamentos de memória Java, é o Eclipse MAT.

● O Que o MAT Pode Mostrar

  • Quais objetos consomem mais memória
  • Quais caminhos de referência mantêm os objetos vivos
  • Por que os objetos não estão sendo liberados
  • Inchaço de coleções
  • Relatórios automáticos de “Suspeitos de Vazamento”

● Passos Básicos de Análise

  1. Abra o dump de heap (*.hprof)
  2. Execute o “Relatório de Suspeitos de Vazamento”
  3. Encontre coleções que retêm grandes quantidades de memória
  4. Verifique a Árvore Dominadora para identificar objetos “pai”
  5. Siga o caminho de referência (“Path to GC Root”)

8-6. Se Você Entender a Árvore Dominadora, a Análise Acelera Dramaticamente

A Árvore Dominadora ajuda a identificar objetos que dominam (controlam) grandes porções do uso de memória.

Exemplos incluem:

  • Um ArrayList massivo
  • Um HashMap com um número enorme de chaves
  • Um cache que nunca é liberado
  • Um singleton mantido por static

Encontrar esses objetos pode reduzir drasticamente o tempo para localizar o vazamento.

8-7. Como Capturar um Dump de Heap (Linha de Comando)

Você também pode capturar um dump de heap usando jmap:

jmap -dump:format=b,file=heap.hprof <PID>

É possível ainda configurar a JVM para despejar o heap automaticamente quando ocorrer OOM:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof

Isso é essencial para investigação de incidentes em produção.

8-8. Correção Real de Vazamento Exige Alterações no Código

Se houver um vazamento, medidas como:

  • Aumentar o tamanho do heap
  • Ajustar o GC

são apenas medidas temporárias de suporte vital.

Em última análise, são necessárias mudanças de design, como:

  • Corrigir a parte que mantém referências indefinidamente
  • Revisitar o design das coleções
  • Evitar o uso excessivo de static
  • Implementar a expulsão e limpeza de caches

8-9. Como Diferenciar “Escassez de Heap” vs “Vazamento de Memória”

● Em um Caso de Escassez de Heap

  • OOM ocorre rapidamente à medida que o volume de dados aumenta
  • Escala com a carga de trabalho
  • Aumentar o heap estabiliza o sistema

● Em um Caso de Vazamento de Memória

  • OOM ocorre após longo tempo de atividade
  • Conforme as requisições aumentam, o desempenho piora gradualmente
  • A memória mal diminui mesmo após GC completo
  • Aumentar o heap não resolve o problema

8-10. Resumo: Se o Ajuste do Heap Não Resolver o OOM, Suspeite de Vazamento

Entre os problemas de “java heap space”, a causa raiz que mais consome tempo para ser identificada costuma ser um vazamento de memória.

Mas com VisualVM + Eclipse MAT, muitas vezes é possível descobrir em minutos:

  • Objetos que consomem mais memória
  • As referências raiz que os mantêm vivos
  • A origem do inchaço de coleções

9. Problemas de “java heap space” em Docker / Kubernetes e Como Corrigi‑los

Aplicações Java modernas cada vez mais são executadas não apenas em ambientes on‑prem, mas também em Docker e Kubernetes (K8s).
Entretanto, como os ambientes de contêiner utilizam um modelo de memória diferente do host, há muitos pontos fáceis de serem mal compreendidos pelos desenvolvedores Java, e erros de “java heap space” ou OOMKilled (terminação forçada do contêiner) podem ocorrer com frequência.

Esta seção resume o gerenciamento de memória específico de contêineres e as configurações que você deve conhecer em operações reais.

9-1. Por que Erros de Heap Space São Tão Comuns em Contêineres

A razão é simples:

Java pode não reconhecer corretamente os limites de memória do contêiner.

● Um Equívoco Comum

“Como defini um limite de memória no Docker --memory=512m, o Java deve rodar dentro de 512 MB.”

→ Na prática, essa suposição pode estar errada.

Ao decidir o tamanho do heap, o Java pode referenciar a memória física do host em vez dos limites do contêiner.

Como resultado:

  • O Java decide “o host tem muita memória”
  • Tenta alocar um heap maior
  • Quando ultrapassa o limite do contêiner, o OOM Killer entra em ação e o processo é terminado à força

9-2. Melhorias no Java 8u191+ e Java 11+

A partir de certas atualizações do Java 8 e no Java 11+, foi introduzido o “UseContainerSupport”.

● Comportamento em Contêineres

  • Pode reconhecer limites baseados em cgroup
  • Calcula automaticamente o tamanho do heap dentro desses limites

Entretanto, o comportamento ainda varia conforme a versão, portanto a configuração explícita é recomendada em produção.

9-3. Definindo Explicitamente o Tamanho do Heap em Contêineres (Obrigatório)

● Padrão de Inicialização Recomendado

docker run \
  --memory=1g \
  -e JAVA_OPTS="-Xms512m -Xmx800m" \
  my-java-app

Pontos chave:

  • Memória do contêiner: 1 GB
  • Heap Java: mantenha‑o dentro de 800 MB
  • O restante é usado por pilhas de threads e memória nativa

● Exemplo Ruim (Muito Comum)

docker run --memory=1g my-java-app   # no -Xmx

→ O Java pode alocar heap com base na memória do host e, ao ultrapassar 1 GB, você obtém OOMKilled.

9-4. Armadilhas de Configuração de Memória no Kubernetes (K8s)

No Kubernetes, resources.limits.memory é crítico.

● Exemplo de Pod

resources:
  limits:
    memory: "1024Mi"
  requests:
    memory: "512Mi"

Nesse caso, manter o -Xmx do Java em torno de 800 MB a 900 MB costuma ser mais seguro.

● Por que Definir um Valor Inferior ao Limite?

Porque o Java usa mais que o heap:

  • Memória nativa
  • Pilhas de threads (centenas de KB × número de threads)
  • Metaspace
  • Sobrecarga dos workers de GC
  • Código JIT‑compiled
  • Carregamento de bibliotecas

Juntos, esses componentes podem consumir facilmente 100–300 MB.

Na prática, uma regra comum é:

Se o limite = X, defina -Xmx para aproximadamente X × 0,7 a 0,8 por segurança.

9-5. Percentual Automático de Heap no Java 11+ (MaxRAMPercentage)

No Java 11, o tamanho do heap pode ser calculado automaticamente usando regras como:

● Configurações Padrão

-XX:MaxRAMPercentage=25
-XX:MinRAMPercentage=50

Significando:

  • O heap é limitado a 25 % da memória disponível
  • Em ambientes pequenos, pode usar ao menos 50 % como heap

● Configuração Recomendada

Em contêineres, costuma ser mais seguro definir explicitamente o MaxRAMPercentage:

JAVA_OPTS="-XX:MaxRAMPercentage=70"

9-6. Por que OOMKilled Acontece Tão Frequentemente em Contêineres (Padrão do Mundo Real)

Um padrão comum em produção:

  1. Limite de memória do K8s = 1 GB
  2. Nenhum -Xmx configurado
  3. O Java referencia a memória do host e tenta alocar mais de 1 GB de heap
  4. O contêiner é terminado à força → OOMKilled

Observe que isso não é necessariamente um evento de java heap space (OutOfMemoryError) — trata‑se de uma terminação OOM ao nível do contêiner.

9-7. Pontos de Verificação Específicos de Contêiner Usando Logs de GC e Métricas

Em ambientes de contêiner, foque especialmente em:

  • Se o número de reinicializações de pods está aumentando
  • Se eventos de OOMKilled estão sendo registrados
  • Se a geração Old continua crescendo
  • Se a recuperação de GC cai abruptamente em determinados momentos
  • Se a memória nativa (não‑heap) está se esgotando

Prometheus + Grafana torna isso muito mais fácil de visualizar.

9-8. Resumo: “Configurações Explícitas” São o Padrão em Contêineres

  • --memory sozinho pode não levar o Java a calcular o heap corretamente
  • Sempre defina -Xmx
  • Deixe margem para memória nativa e pilhas de threads
  • Defina valores inferiores aos limites de memória do Kubernetes
  • No Java 11+, MaxRAMPercentage pode ser útil

10. Anti-Padrões a Evitar (Código Ruim / Configurações Ruins)

O erro de “java heap space” acontece não apenas quando o heap é realmente insuficiente, mas também quando certos padrões de codificação perigosos ou configurações incorretas são usados.

Aqui resumimos anti-padrões comuns vistos frequentemente no trabalho real.

10-1. Deixando Coleções Ilimitadas Crescem Para Sempre

Um dos problemas mais frequentes é o inchaço de coleção.

● Exemplo Ruim: Adicionando a uma Lista Sem Qualquer Limite

List<String> logs = new ArrayList<>();
while (true) {
    logs.add(getMessage());  // ← grows forever
}

Com longo tempo de atividade, isso sozinho pode facilmente levá-lo a OOM.

● Por Que É Perigoso

  • GC não pode recuperar memória, e a geração Old incha
  • Full GC se torna frequente, tornando o app mais propenso a congelar
  • Copiar números massivos de objetos aumenta a carga de CPU

● Como Evitá-lo

  • Defina um limite de tamanho (ex.: um cache LRU)
  • Limpe periodicamente
  • Não retenha dados desnecessariamente

10-2. Carregando Arquivos ou Dados Enormes de Uma Só Vez

Este é um erro comum em processamento em lote e side do servidor.

● Exemplo Ruim: Lendo um JSON Enorme de Uma Vez

String json = Files.readString(Paths.get("large.json"));
Data d = mapper.readValue(json, Data.class);

● O Que Dá Errado

  • Você retém tanto a string pré-parse quanto os objetos pós-parse na memória
  • Um arquivo de 500MB pode consumir bem mais que o dobro disso na memória
  • Objetos intermediários adicionais são criados, e o heap se esgota

● Como Evitá-lo

  • Use streaming (processamento sequencial)
  • Leia em pedaços em vez de carregamento em massa
  • Não retenha o conjunto de dados completo na memória

10-3. Continuando a Manter Dados em Variáveis static

● Exemplo Ruim

public class UserCache {
    private static Map<String, User> cache = new HashMap<>();
}

● Por Que É Perigoso

  • static vive enquanto o JVM está rodando
  • Se usado como cache, entradas podem nunca ser liberadas
  • Referências permanecem, tornando-se um terreno fértil para vazamentos de memória

● Como Evitá-lo

  • Mantenha o uso de static ao mínimo
  • Use um framework de cache dedicado (ex.: Caffeine)
  • Defina TTL e um limite de tamanho máximo

10-4. Usando Excessivamente Stream / Lambda e Gerando Listas Intermediárias Enormes

A API Stream é conveniente, mas pode criar objetos intermediários internamente e pressionar a memória.

● Exemplo Ruim (collect cria uma lista intermediária massiva)

List<Item> result = items.stream()
        .map(this::convert)
        .collect(Collectors.toList());

● Como Evitá-lo

  • Processe sequencialmente com um for-loop
  • Evite gerar listas intermediárias desnecessárias
  • Se o conjunto de dados for grande, reconsidere usar Stream nessa parte

10-5. Fazendo Concatenação Massiva de Strings com o Operador +

Como Strings são imutáveis, cada concatenação cria um novo objeto String.

● Exemplo Ruim

String result = "";
for (String s : list) {
    result += s;
}

● O Que Está Errado

  • Uma nova String é criada a cada iteração
  • Um número enorme de instâncias é produzido, pressionando a memória

● Como Evitá-lo

StringBuilder sb = new StringBuilder();
for (String s : list) {
    sb.append(s);
}

10-6. Criando Muitos Caches e Não Gerenciando-os

● Exemplos Ruins

  • Armazenando respostas de API em um Map indefinidamente
  • Cacheando continuamente imagens ou dados de arquivo
  • Sem mecanismo de controle como LRU

● Por Que É Arriscado

  • O cache cresce ao longo do tempo
  • Memória não recuperável aumenta
  • Isso quase sempre se tornará um problema em produção

● Como Evitá-lo

  • Use Caffeine / Guava Cache
  • Defina um tamanho máximo
  • Configure TTL (expiração)

10-7. Mantendo Logs ou Estatísticas na Memória Continuamente

● Exemplo Ruim

List<String> debugLogs = new ArrayList<>();
debugLogs.add(message);

Em produção, os logs devem ser gravados em arquivos ou sistemas de log. Mantê‑los na memória é arriscado.

10-8. Não Especificar -Xmx em Contêineres Docker

Isso representa uma grande parte dos incidentes modernos relacionados ao heap.

● Exemplo Ruim

docker run --memory=1g my-app

● O Que Está Errado

  • O Java pode dimensionar automaticamente o heap com base na memória do host
  • Quando ultrapassa o limite do contêiner, ocorre OOMKilled

● Como Evitar Isso

docker run --memory=1g -e JAVA_OPTS="-Xmx700m"

10-9. Ajuste Excessivo das Configurações de GC

Um ajuste incorreto pode ter efeito contrário.

● Exemplo Ruim

-XX:MaxGCPauseMillis=10
-XX:G1HeapRegionSize=1m

Parâmetros extremos podem tornar o GC excessivamente agressivo ou impedir que ele acompanhe a carga.

● Como Evitar Isso

  • Na maioria dos casos, as configurações padrão são suficientes
  • Ajuste apenas minimamente quando houver um problema específico e mensurado

10-10. Resumo: A Maioria dos Anti‑Padrões Vêm de “Armazenar Demais”

O que todos esses anti‑padrões têm em comum é:

“Acumular mais objetos do que o necessário.”

  • Coleções sem limite
  • Retenção desnecessária
  • Carregamento em massa
  • Designs com uso intensivo de estáticos
  • Cache descontrolado
  • Explosão de objetos intermediários

Evitar esses itens por si só pode reduzir drasticamente os erros de “java heap space”.

11. Exemplos Reais: Este Código é Perigoso (Padrões Típicos de Problemas de Memória)

Esta seção apresenta exemplos de código perigosos frequentemente encontrados em projetos reais que costumam levar a erros de “java heap space”, e explica para cada um:
“Por que é perigoso” e “Como corrigi‑lo.”

Na prática, esses padrões costumam ocorrer juntos, portanto este capítulo é extremamente útil para revisões de código e investigações de incidentes.

11-1. Carregamento em Massa de Dados Enormes

● Exemplo Ruim: Ler Todas as Linhas de um CSV Enorme

List&lt;String&gt; lines = Files.readAllLines(Paths.get("big.csv"));

● Por Que É Perigoso

  • Quanto maior o arquivo, maior a pressão de memória
  • Mesmo um CSV de 100 MB pode consumir mais do que o dobro da memória antes/depois da análise
  • Reter registros massivos pode esgotar a geração Old

● Melhoria: Ler via Stream (Processamento Sequencial)

try (Stream<String> stream = Files.lines(Paths.get("big.csv"))) {
    stream.forEach(line -> process(line));
}

→ Apenas uma linha é mantida na memória por vez, tornando isso muito seguro.

11-2. Padrão de Inchaço de Coleção

● Exemplo Ruim: Acumular Continuamente Objetos Pesados em uma Lista

List<Order> orders = new ArrayList<>();
while (hasNext()) {
    orders.add(fetchNextOrder());
}

● Por Que É Perigoso

  • Cada etapa de crescimento realoca o array interno
  • Se você não precisa manter tudo, é puro desperdício
  • Execuções longas podem consumir um enorme espaço da geração Old

● Melhoria: Processar Sequencialmente + Em Lotes Quando Necessário

while (hasNext()) {
    Order order = fetchNextOrder();
    process(order);      // process without retaining
}

Ou em lote:

List<Order> batch = new ArrayList<>(1000);
while (hasNext()) {
    batch.add(fetchNextOrder());
    if (batch.size() == 1000) {
        processBatch(batch);
        batch.clear();
    }
}

11-3. Gerando Muitos Objetos Intermediários via API Stream

● Exemplo Ruim: Listas intermediárias repetidas via map → filter → collect

List<Data> result = list.stream()
        .map(this::convert)
        .filter(d -> d.isValid())
        .collect(Collectors.toList());

● Por Que É Perigoso

  • Cria muitos objetos temporários internamente
  • Especialmente arriscado com listas enormes
  • Quanto mais profunda a cadeia, maior o risco

● Melhoria: Use um loop for ou processamento sequencial

List<Data> result = new ArrayList<>();
for (Item item : list) {
    Data d = convert(item);
    if (d.isValid()) {
        result.add(d);
    }
}

11-4. Analisando JSON ou XML de Uma Vez

● Exemplo Ruim

String json = Files.readString(Paths.get("large.json"));
Data data = mapper.readValue(json, Data.class);

● Por Que É Perigoso

  • • Tanto a string JSON bruta quanto os objetos desserializados permanecem na memória
  • • Com arquivos de classe de 100 MB, o heap pode encher instantaneamente
  • • Problemas semelhantes podem ocorrer mesmo ao usar APIs de Stream, dependendo do uso

● Melhoria: Use uma API de Streaming

JsonFactory factory = new JsonFactory();
try (JsonParser parser = factory.createParser(new File("large.json"))) {
    while (!parser.isClosed()) {
        JsonToken token = parser.nextToken();
        // Process only when needed and do not retain data
    }
}

11-5. Carregando Todas as Imagens / Dados Binários na Memória

● Exemplo Ruim

byte[] image = Files.readAllBytes(Paths.get("large.png"));

● Por Que É Perigoso

  • • Dados binários podem ser grandes e “pesados” por natureza
  • • Em aplicativos de processamento de imagens, isso é uma das principais causas de OOM

● Melhorias

  • • Use bufferização
  • • Processar como um fluxo sem reter o arquivo inteiro na memória
  • • A leitura em massa de logs com milhões de linhas é igualmente perigosa

11-6. Retenção Infinita via Cache estático

● Exemplo Ruim

private static final List<Session> sessions = new ArrayList<>();

● O Que Está Errado

  • sessions não será liberado até que a JVM seja encerrada
  • • Ele cresce com as conexões e eventualmente leva a OOM

● Melhorias

  • • Use um cache gerenciado por tamanho (Caffeine, Guava Cache, etc.)
  • • Gerencie claramente o ciclo de vida da sessão

11-7. Uso Indevido de ThreadLocal

● Exemplo Ruim

private static final ThreadLocal<SimpleDateFormat> formatter =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

ThreadLocal é útil, mas com pools de threads pode manter valores vivos e causar vazamentos.

● Melhorias

  • • Mantenha ThreadLocal de curta duração
  • • Evite usá-lo a menos que seja realmente necessário
  • • Chame remove() para limpá-lo

11-8. Criando Muitas Exceções

Isso costuma ser negligenciado, mas Exceções são objetos muito pesados devido à geração de rastreamento de pilha.

● Exemplo Ruim

for (...) {
    try {
        doSomething();
    } catch (Exception e) {
        // log only
    }
}

→ Inundar exceções pode pressionar a memória.

● Melhorias

  • • Não use exceções para fluxo de controle normal
  • • Rejeite entradas inválidas via validação
  • • Evite lançar exceções a menos que seja necessário

11-9. Resumo: Código Perigoso “Silenciosamente” Consome Seu Heap

O tema comum é: “estruturas que gradualmente comprimem o heap, empilhadas umas sobre as outras.”

  • • Carregamento em massa
  • • Coleções infinitas
  • • Esquecer de cancelar registro/limpar
  • • Criação de objetos intermediários
  • • Inundação de exceções
  • • retenção estática
  • • resíduos de ThreadLocal

Em todos os casos, o impacto torna‑se óbvio durante execuções longas.

12. Melhores Práticas de Gerenciamento de Memória Java (Essenciais para Prevenir Recorrência)

Até agora, cobrimos as causas dos erros de “java heap space” e contramedidas como expansão do heap, melhorias de código, ajuste de GC e investigação de vazamentos.

Esta seção resume as melhores práticas que evitam recorrência de forma confiável em operações reais. Considere‑as como as regras mínimas para manter aplicações Java estáveis.

12-1. Defina o Tamanho do Heap Explicitamente (Especialmente em Produção)

Executar cargas de trabalho de produção com as configurações padrão é arriscado.

● Melhores Práticas

  • • Defina explicitamente -Xms e -Xmx
  • • Não execute produção com as configurações padrão
  • • Mantenha os tamanhos de heap consistentes entre dev e prod (evite diferenças inesperadas)

Exemplo:

-Xms1g -Xmx1g

No Docker / Kubernetes, você deve definir o heap menor para corresponder aos limites do contêiner.

12-2. Monitore Adequadamente (GC, Uso de Memória, OOM)

Problemas de heap são frequentemente evitáveis se você detectar sinais de alerta precocemente.

● O que Monitorar

  • Uso da geração antiga
  • Tendências de crescimento da geração jovem
  • Frequência de GC completo
  • Tempo de pausa do GC
  • Eventos OOMKilled do contêiner
  • Contagem de reinicializações de pods (K8s)

● Ferramentas Recomendadas

  • VisualVM
  • JDK Mission Control
  • Prometheus + Grafana
  • Métricas do provedor de nuvem (ex.: CloudWatch)

Um aumento gradual no uso de memória ao longo de longas execuções é um sinal clássico de vazamento.

12-3. Use “Caches Controladas”

O crescimento descontrolado de cache é uma das causas mais comuns de OOM em produção.

● Boas Práticas

  • Use Caffeine / Guava Cache
  • Sempre configure TTL (expiração)
  • Defina um tamanho máximo (ex.: 1.000 entradas)
  • Evite caches estáticos tanto quanto possível
    Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build();
    

12-4. Tenha Cuidado com o Uso Excessivo da Stream API e Lambdas

Para grandes conjuntos de dados, encadear operações de Stream aumenta objetos intermediários.

● Boas Práticas

  • Não encadeie map/filter/collect mais do que o necessário
  • Processar grandes conjuntos de dados sequencialmente com um loop for
  • Ao usar collect, esteja ciente do volume de dados

Streams são convenientes, mas nem sempre são amigáveis à memória.

12-5. Converta Arquivos Grandes / Dados Grandes para Streaming

Processamento em lote é uma causa raiz importante de problemas de heap.

● Boas Práticas

  • CSV → Files.lines()
  • JSON → Jackson Streaming
  • BD → paginação
  • API → busca em blocos (cursor/paginação)

Se você aplicar “não carregue tudo na memória”, muitos problemas de heap desaparecem.

12-6. Trate ThreadLocal com Cuidado

ThreadLocal é poderoso, mas o uso indevido pode causar vazamentos de memória graves.

● Boas Práticas

  • Tenha especial cuidado quando combinado com pools de threads
  • Chame remove() após o uso
  • Não armazene dados de longa duração
  • Evite ThreadLocal estático sempre que possível

12-7. Capture Dumps de Heap Periodicamente para Detectar Vazamentos Cedo

Para sistemas de longa execução (aplicações web, sistemas batch, IoT), capturar dumps de heap regularmente e compará-los ajuda a detectar sinais de vazamento precocemente.

● Opções

  • VisualVM
  • jmap
  • -XX:+HeapDumpOnOutOfMemoryError
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=/var/log/heapdump.hprof
    

Dump automático em OOM é uma configuração indispensável em produção.

12-8. Mantenha o Ajuste de GC Mínimo

A ideia de que “ajustar o GC aumentará automaticamente o desempenho” pode ser perigosa.

● Boas Práticas

  • Comece com as configurações padrão
  • Faça mudanças mínimas somente quando existir um problema mensurado
  • Use G1GC como a escolha padrão
  • Em muitos casos, aumentar o heap é mais eficaz do que microajustes

12-9. Considere Dividir a Arquitetura

Se os volumes de dados se tornarem muito grandes ou a aplicação ficar muito monolítica e exigir um heap massivo, pode ser necessário melhorar a arquitetura:

  • Microserviços
  • Divisão do processamento de dados batch
  • Desacoplamento com filas de mensagens (Kafka, etc.)
  • Processamento distribuído (Spark, etc.)

Se “não importa quanto heap você adicione, nunca é suficiente”, suspeite de um problema arquitetural.

12-10. Resumo: Gerenciamento de Memória Java é Sobre Otimização em Camadas

Problemas de heap raramente são resolvidos por uma única configuração ou correção de código.

● Principais Conclusões

  • Sempre defina o heap explicitamente
  • Monitoramento é o mais importante
  • Nunca permita inchaço de coleta
  • Use streaming para grandes volumes de dados
  • Gerencie caches adequadamente
  • Use ThreadLocal com cuidado
  • Analise vazamentos com ferramentas quando necessário
  • Contêineres exigem uma mentalidade diferente

Seguir esses pontos evitará a maioria dos erros de “java heap space” com alta certeza.

13. Resumo: Pontos-Chave para Prevenir Erros de “java heap space”

Neste artigo, abordamos erros de “java heap space” desde as causas raiz até mitigação e prevenção de recorrência.

Aqui organizamos os essenciais como um resumo prático.

13-1. O Problema Real Não é “Heap Muito Pequeno”, mas “Por Que Está Esgotando?”

“java heap space” não é apenas uma simples escassez de memória.

● A causa raiz costuma ser uma das seguintes

  • Tamanho do heap muito pequeno (configuração insuficiente)
  • Processamento em lote de dados enormes (problema de design)
  • Inchaço de coleções (falta de exclusão/design)
  • Vazamento de memória (referências permanecem)
  • Configuração incorreta em contêineres (específico de Docker/K8s)

Inicie com: “Por que o heap acabou?”

13-2. Primeiros Passos para Investigar

① Confirmar o dimensionamento do heap

→ Defina explicitamente -Xms / -Xmx

② Entender as restrições de memória em tempo de execução

→ No Docker/Kubernetes, alinhe limites e dimensionamento do heap
→ Também verifique -XX:MaxRAMPercentage

③ Capturar e inspecionar logs de GC

→ Crescimento da geração antiga e Full GC frequente são sinais de alerta

④ Capturar e analisar dumps de heap

→ Use VisualVM / MAT para obter evidências de vazamentos

13-3. Padrões de Alto Risco Comuns em Produção

Como mostrado ao longo deste artigo, os seguintes padrões frequentemente levam a incidentes:

  • Processamento em lote de arquivos enormes
  • Adicionar a List/Map sem limite
  • Cache descontrolado
  • Acumular dados em estáticos
  • Objetos intermediários explosivos via cadeias de Stream
  • Uso indevido de ThreadLocal
  • Não definir -Xmx no Docker

Se você encontrar esses padrões no código ou nas configurações, investigue primeiro.

13-4. Correções Fundamentais São Sobre Design de Sistema e Processamento de Dados

● O que revisar no nível do sistema

  • Trocar o tratamento de grandes volumes de dados por processamento em streaming
  • Usar caches com TTL, limites de tamanho e políticas de expulsão
  • Realizar monitoramento regular de memória para aplicações de longa execução
  • Analisar sinais iniciais de vazamento com ferramentas

● Se ainda for difícil

  • Separar processamento batch de online
  • Microserviços
  • Adotar plataformas de processamento distribuído (Spark, Flink, etc.)

Melhorias arquiteturais podem ser necessárias.

13-5. As Três Mensagens Mais Importantes

Se você lembrar de apenas três coisas:

✔ Sempre defina o heap explicitamente

✔ Nunca processe em lote dados enormes

✔ Você não pode confirmar vazamentos sem dumps de heap

Apenas essas três podem reduzir drasticamente incidentes críticos de produção causados por “java heap space”.

13-6. Gerenciamento de Memória Java É uma Habilidade que Cria uma Vantagem Real

Gerenciar memória Java pode parecer difícil, mas se você o entender:

  • A investigação de incidentes se torna dramaticamente mais rápida
  • Sistemas de alta carga podem ser executados de forma estável
  • O ajuste de desempenho se torna mais preciso
  • Você se torna um engenheiro que entende tanto a aplicação quanto a infraestrutura

Não é exagero dizer que a qualidade do sistema é proporcional ao entendimento de memória.

14. FAQ

Por fim, aqui está uma seção prática de Perguntas & Respostas cobrindo dúvidas comuns que as pessoas pesquisam sobre “java heap space”.

Isso complementa o artigo e ajuda a capturar uma gama mais ampla de intenção dos usuários.

Q1. Qual a diferença entre java.lang.OutOfMemoryError: Java heap space e GC overhead limit exceeded?

● java heap space

  • Ocorre quando o heap está fisicamente esgotado
  • Frequentemente causado por dados enormes, inchaço de coleções ou configurações insuficientes

● GC overhead limit exceeded

  • O GC está trabalhando intensamente, mas recuperando quase nada
  • Um sinal de que o GC não consegue recuperar memória devido a muitos objetos vivos
  • Geralmente indica um vazamento de memória ou referências persistentes

Um modelo mental útil:
heap space = já ultrapassou o limite,
GC overhead = logo antes do limite.

Q2. Se eu simplesmente aumentar o heap, isso resolve?

✔ Pode ajudar temporariamente

✘ Não resolve a causa raiz

  • Se o heap for realmente pequeno para sua carga de trabalho → ajuda
  • Se coleções ou vazamentos forem a causa → o problema retornará

Se a causa for um vazamento, dobrar o heap apenas adia o próximo OOM.

Q3. Quanto posso aumentar o heap do Java?

● Normalmente: 50%–70% da memória física

Porque você deve reservar memória para:

  • Memória nativa
  • Pilhas de threads
  • Metaspace
  • Workers de GC
  • Processos do SO

Especialmente em Docker/K8s, a prática comum é definir:
-Xmx = 70%–80% do limite do contêiner.

Q4. Por que o Java recebe OOMKilled em contêineres (Docker/K8s)?

● Na maioria dos casos, porque -Xmx não está definido

O Docker pode não repassar os limites do contêiner para o Java de forma limpa, então o Java dimensiona o heap com base na memória do host → ultrapassa o limite → OOMKilled.

✔ Correção

docker run --memory=1g -e JAVA_OPTS="-Xmx800m"

Q5. Existe uma maneira fácil de saber se é um vazamento de memória?

✔ Se estas condições forem verdadeiras, é muito provável que seja um vazamento

  • O uso do heap continua aumentando com o tempo de atividade
  • A memória quase não diminui mesmo após um Full GC
  • A geração old cresce em um padrão de “degraus”
  • O OOM ocorre após horas ou dias
  • Execuções curtas parecem normais

Entretanto, a confirmação final requer análise de dump de heap (Eclipse MAT).

Q6. Configurações de heap no Eclipse / IntelliJ não são aplicadas

● Causas comuns

  • Você não editou a Configuração de Execução
  • As configurações padrão da IDE estão prevalecendo
  • Outro script de inicialização com JAVA_OPTS sobrescreve suas configurações
  • Você esqueceu de reiniciar o processo

As configurações da IDE variam, portanto verifique sempre o campo “VM options” na Configuração de Execução/Depuração.

Q7. É verdade que o Spring Boot consome muita memória?

Sim. O Spring Boot costuma consumir mais memória devido a:

  • Autoconfiguração
  • Muitos Beans
  • Carregamento de classes em JARs “fat”
  • Servidor web embutido (Tomcat, etc.)

Em comparação com um programa Java simples, pode usar cerca de 200–300 MB a mais em alguns casos.

Q8. Qual GC devo usar?

Na maioria dos casos, G1GC é a opção segura padrão.

● Recomendações por tipo de carga

  • Aplicações web → G1GC
  • Jobs em lote com alta taxa de transferência → Parallel GC
  • Necessidades de latência ultra‑baixa → ZGC / Shenandoah

Sem um motivo forte, escolha G1GC.

Q9. Como devo lidar com o heap em ambientes serverless (Cloud Run / Lambda)?

Ambientes serverless têm limites de memória apertados, portanto você deve configurar o heap explicitamente.

Exemplo (Java 11):

-XX:MaxRAMPercentage=70

Observe também que a memória pode disparar durante cold starts, então deixe uma margem de segurança na configuração do heap.

Q10. Como posso evitar que problemas de heap em Java se repitam?

Se você seguir rigorosamente estas três regras, a recorrência diminui drasticamente:

✔ Defina o heap explicitamente

✔ Processar grandes volumes de dados por streaming

✔ Revise regularmente logs de GC e dumps de heap

Resumo: Use o FAQ para Eliminar Dúvidas e Aplicar Medidas Práticas de Controle de Memória

Este FAQ abordou perguntas comuns originadas de buscas sobre “java heap space” com respostas práticas.

Junto ao artigo principal, ele deve ajudá‑lo a se tornar mais competente no tratamento de problemas de memória Java e melhorar significativamente a estabilidade do sistema em produção.