1. Visão geral da memória de um processo Java
Quando você executa um programa Java, a JVM (Java Virtual Machine) pede um pedaço de memória ao sistema operacional. Às vezes — modesto, às vezes — bastante grande (especialmente se você estiver rodando, por exemplo, Minecraft com um monte de mods). Essa memória é dividida em várias áreas-chave, cada uma com seu papel:
- Pilha (Stack) — para variáveis locais e chamadas de métodos.
- Heap — para todos os objetos que você cria com new.
- Áreas de serviço (PermGen/MetaSpace) — para metadados de classes, campos estáticos e outras coisas “mágicas”.
Isso se parece aproximadamente com isto:
┌───────────────────────────────┐
│ Processo JVM │
│ ┌─────────────┐ │
│ │ Stack │ ← Cada thread tem sua própria pilha!
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Heap │ ← Compartilhado por todas as threads
│ └─────────────┘ │
│ ┌───────────────┐ │
│ │ PermGen/ │ ← Metadados de classes
│ │ MetaSpace │
│ └───────────────┘ │
└───────────────────────────────┘
Por que isso é importante?
- Entender a estrutura de memória ajuda a escrever código mais eficiente e seguro.
- Fica mais fácil diagnosticar erros como StackOverflowError ou OutOfMemoryError.
- “Coletor de lixo” e “vazamento de memória” deixam de assustar — você entende onde e o que procurar.
2. Pilha (Stack): rápida, local, mas não para sempre
A pilha é uma área especial de memória alocada para cada thread separadamente. A pilha é como uma pilha de pratos: o último que você colocou — você só vai pegar depois de remover todos os que estão por cima. Ou seja, a pilha funciona pelo princípio LIFO (Last In, First Out).
Para que serve a pilha?
Na pilha armazenam-se:
- Variáveis locais dos métodos (por exemplo, int x = 5; dentro de um método).
- Endereço de retorno após a chamada de um método (para saber para onde voltar após o término do método).
Toda vez que você chama um método, é adicionado à pilha um novo frame (stack frame) — algo como uma caixa, onde ficam todas as variáveis locais desse método e informações de serviço. Quando o método termina, seu frame é removido — todas as suas variáveis locais desaparecem.
Exemplo
public static void main(String[] args) {
int a = 10; // a fica na pilha de main
int b = sum(a, 5); // chamamos sum
}
public static int sum(int x, int y) {
int result = x + y; // x, y e result ficam na pilha de sum
return result;
}
- Quando sum é chamado, é criado um frame separado para ele na pilha.
- Depois que sum termina, suas variáveis desaparecem.
Ciclo de vida de uma variável
Variáveis locais vivem apenas enquanto o método em que foram declaradas está em execução. Assim que o método terminou — elas já não existem mais, a memória é liberada instantaneamente.
Estouro de pilha
Se você acidentalmente (ou de propósito) escreveu uma recursão infinita, cada chamada do método adicionará um novo frame à pilha. Em algum momento a pilha vai acabar, e você receberá:
Exception in thread "main" java.lang.StackOverflowError
Exemplo:
public static void main(String[] args) {
recurse();
}
public static void recurse() {
recurse(); // Recursão infinita!
}
Tamanho da pilha
O tamanho da pilha é limitado — normalmente são alguns megabytes por thread (pode ser definido com o parâmetro -Xss). Se a pilha se esgotar — o programa cai com erro.
3. Heap: lugar para seus objetos
O heap é uma área de memória compartilhada por todas as threads, onde vivem todos os objetos que você cria com new, assim como os arrays. É no heap que acontece toda a mágica da programação orientada a objetos.
Como os objetos vão para o heap?
String s = new String("Hello");
int[] arr = new int[10];
- A variável s — é uma referência, ela fica na pilha.
- O próprio objeto String e o array arr — ficam no heap.
Ciclo de vida de um objeto
Um objeto vive no heap enquanto existir pelo menos uma referência forte (strong reference) para ele. Assim que ninguém mais se referir ao objeto — ele vira “lixo” e pode ser removido pelo coletor de lixo (GC).
Gerenciamento de memória
Diferentemente de C/C++, onde você mesmo deve cuidar da liberação de memória (free, delete), em Java isso é feito pelo GC. Você não pode liberar um objeto explicitamente, mas pode zerar todas as referências a ele — então ele se tornará candidato à remoção.
Esquema: onde fica o quê?
Stack (main)
└─ s ─┬────────────┐
│ │
▼ │
Heap │
┌─────────────┐ │
│ String "Hello"◄──┘
└─────────────┘
Particularidades do heap
- O heap é único para todo o processo da JVM.
- O tamanho do heap pode ser definido na inicialização (-Xmx, -Xms).
- Se não restar espaço livre no heap e o GC não conseguir liberar memória — o programa cai com o erro OutOfMemoryError.
4. PermGen e MetaSpace: onde vivem as classes?
Quando você escreve class MyClass { ... }, e depois executa o programa, a JVM precisa guardar em algum lugar tudo o que está associado a essa classe — métodos, campos, bytecode, variáveis estáticas, constantes e até literais de string. Para isso existe na JVM uma área especial de memória onde as classes “vivem”.
Antigamente, antes do Java 8, essa área era chamada de PermGen (Permanent Generation). Mas ela tinha vários problemas — por exemplo, possuía tamanho fixo e, se o espaço não fosse suficiente, o aplicativo simplesmente encerrava com o erro OutOfMemoryError: PermGen space.
Com o Java 8 surgiu uma nova área, mais flexível — MetaSpace. Ela substituiu a antiga PermGen e agora pode se expandir automaticamente, ocupando tanta memória quanto necessário (dentro da memória física disponível).
PermGen (antes do Java 8)
- No PermGen ficavam metadados de classes, campos estáticos, literais de string.
- O tamanho do PermGen era limitado (pequeno por padrão), e podia ser aumentado com o parâmetro -XX:MaxPermSize=256m.
- Se o aplicativo carregasse muitas classes dinamicamente (por exemplo, em servidores web), o PermGen podia “se esgotar”, e você recebia o erro:
java.lang.OutOfMemoryError: PermGen space
- Problema: a limpeza do PermGen nem sempre acontecia corretamente quando as classes eram descarregadas dinamicamente (por exemplo, ao recarregar aplicações web).
MetaSpace (Java 8+)
- Com o Java 8 o PermGen desapareceu e surgiu o MetaSpace.
- O MetaSpace armazena metadados de classes, mas agora na memória nativa (fora do Java Heap).
- O tamanho do MetaSpace por padrão não é limitado (é limitado apenas pela memória do sistema), mas é possível definir um limite com -XX:MaxMetaspaceSize=512m.
- O erro por falta de memória agora se parece com isto:
java.lang.OutOfMemoryError: Metaspace
- No MetaSpace também ficam campos estáticos, métodos e informações sobre as classes.
Esquema: como tudo está organizado
┌───────────────────────────────┐
│ Processo JVM │
│ ┌─────────────┐ │
│ │ Stack │ ← Variáveis locais, chamadas de métodos
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Heap │ ← Objetos, arrays, tudo que é via new
│ └─────────────┘ │
│ ┌───────────────┐ │
│ │ MetaSpace │ ← Metadados de classes, campos estáticos
│ └───────────────┘ │
└───────────────────────────────┘
Por que isso é importante?
Se você escreve aplicações desktop ou de servidor comuns, é bem possível que nunca se depare com erros de PermGen ou MetaSpace. Mas se trabalha com carregamento dinâmico de classes (por exemplo, plugins, aplicações web, frameworks como Spring, que podem carregar e descarregar muitas classes), então conhecer o MetaSpace é indispensável!
5. Ilustração: Esquema de memória da JVM
flowchart TD
subgraph JVM
direction TB
Stack1["Stack (Thread 1)"]
Stack2["Stack (Thread 2)"]
Heap[Heap]
MetaSpace[MetaSpace]
end
Stack1 --faz referência a--> Heap
Stack2 --faz referência a--> Heap
Heap --usa classes de--> MetaSpace
- Cada thread tem sua própria pilha.
- Todas as pilhas podem referenciar objetos no heap.
- Os objetos no heap “sabem” sua classe, cuja informação está no MetaSpace.
6. Exemplo: como isso aparece em código real
public class MemoryDemo {
public static void main(String[] args) {
int x = 42; // x fica na pilha de main
String s = "Hello!"; // s — referência na pilha, objeto String no heap, literal "Hello!" no MetaSpace
Person p = new Person("Alice"); // p — referência na pilha, objeto Person no heap
// Vamos chamar o método para criar um novo frame de pilha
printPerson(p);
}
public static void printPerson(Person person) {
// person — referência na pilha de printPerson
System.out.println(person.getName());
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() { return name; }
}
Análise:
- x — variável local, vive na pilha do método main.
- s — referência na pilha, o objeto String no heap, e o literal de string "Hello!" — no MetaSpace.
- p — referência na pilha, o objeto Person no heap.
- A classe Person e todos os seus métodos/campos — no MetaSpace (metadados de classes).
- A chamada printPerson(p) cria um novo frame de pilha; dentro dele a referência local person aponta para o mesmo objeto no heap.
7. Como a JVM gerencia a memória: FAQ rápido
Posso gerenciar a pilha?
Não, a pilha fica totalmente sob controle da JVM. Você pode apenas definir seu tamanho na inicialização (-Xss).
Posso gerenciar o heap?
Parcialmente: o tamanho do heap é definido na inicialização (-Xmx, -Xms). A limpeza fica a cargo do coletor de lixo (GC).
Posso gerenciar o MetaSpace?
Você pode limitar o tamanho (-XX:MaxMetaspaceSize), mas geralmente isso não é necessário.
O que acontece quando falta memória?
— Se a pilha acabar — StackOverflowError.
— Se o heap acabar — OutOfMemoryError: Java heap space.
— Se o MetaSpace acabar — OutOfMemoryError: Metaspace.
8. Erros típicos ao trabalhar com memória
Erro nº 1: StackOverflowError por causa de recursão infinita. O motivo mais comum — esquecer de prever a condição de parada da recursão. Por exemplo, o método chama a si mesmo sem parar. A JVM não consegue expandir a pilha infinitamente, e o programa “cai”.
Erro nº 2: OutOfMemoryError por estouro do heap. Se você cria objetos demais para os quais ainda existem referências (por exemplo, adiciona elementos a uma lista e nunca os remove), o heap pode se esgotar.
Erro nº 3: OutOfMemoryError: PermGen space / Metaspace. Se você usa plugins ou carrega dinamicamente muitas classes, e o MetaSpace não é limpo (por exemplo, por descarregamento incorreto de classes), o espaço no MetaSpace pode terminar.
Erro nº 4: Confusão entre referência e objeto. Muitos iniciantes se confundem: a variável do tipo Person na pilha — é apenas a referência, enquanto o objeto em si está no heap.
Erro nº 5: Esperar que o coletor de lixo remova tudo instantaneamente. O GC trabalha “quando quer” (na verdade — conforme seus algoritmos internos e quando falta memória), e não imediatamente após você zerar a referência. Não conte com liberação imediata da memória.
GO TO FULL VERSION