Java Stack vs. Memória Heap
Como o stack e o heap do Java diferem, o que reside em cada um e o ciclo de vida de variáveis e objetos.
Em tempo de execução, a JVM divide a memória que gerencia em duas regiões com funções muito distintas. O stack armazena o controle das chamadas de método — um frame por chamada, contendo as variáveis locais e os valores primitivos do método. O heap armazena cada objeto criado com new, compartilhado por todo o programa e recuperado pelo garbage collector. Quase todo comportamento confuso do Java — por que um método "não consegue alterar" seu int, por que duas variáveis "enxergam" a mesma edição, por que recursão profunda trava — vem diretamente dessa divisão.
Este capítulo aborda o que reside em cada região, como seus ciclos de vida diferem, por que a regra de passagem por valor do Java decorre diretamente dessa divisão, o caso especial do pool de strings e o que acontece quando qualquer uma das regiões se esgota. Para uma visão mais ampla de como essas regiões se encaixam no tempo de execução, veja Arquitetura da JVM e o Java Memory Model.
Duas regiões, dois ciclos de vida
O stack é por thread e automático: quando um método é chamado, a JVM empurra um frame e, quando o método retorna, esse frame é removido e suas variáveis locais desaparecem instantaneamente. O heap é compartilhado e gerenciado: objetos vivem até que nenhuma referência aponte para eles, momento em que o garbage collector fica livre para recuperar o espaço. Nada no heap desaparece no momento em que um método retorna.
| Aspecto | Stack | Heap |
|---|---|---|
| Armazena | Frames: locais, primitivos, referências | Objetos, arrays, campos de instância |
| Escopo | Um por thread | Um compartilhado por toda a JVM |
| Tempo de vida | Frame removido no retorno do método | Até ficar inacessível, então GC |
| Alocação | Push/pop, extremamente rápido | new, gerenciado pelo alocador |
| Dimensionamento | Limitado (-Xss); overflow lança StackOverflowError | Limitado (-Xmx); esgotamento lança OutOfMemoryError |
| Limpeza | Automática, determinística | Garbage collector, não determinístico |
O que realmente fica em cada lugar
Uma variável local sempre vive no frame de stack atual. O que ela contém depende do seu tipo. Para um primitivo, o frame armazena o próprio valor. Para um tipo de objeto, o frame armazena apenas uma referência — o objeto para o qual ela aponta vive no heap.
void example() {
int count = 5; // the value 5 sits in the frame (stack)
double rate = 0.5; // likewise on the stack
int[] data = new int[3]; // 'data' (a reference) is on the stack,
// the 3-element array is on the heap
Point p = new Point(1, 2); // 'p' is on the stack, the Point is on the heap
} // frame popped: count, rate, data, p all gone;
// the array and Point survive until GCOs campos de instância fazem parte do objeto, portanto vivem no heap junto com ele — mesmo um campo de tipo primitivo. Um private int balance dentro de um objeto Account é memória do heap, não do stack, porque pertence ao objeto, não a nenhuma chamada de método em particular.
Java é passagem por valor — sempre
O Java copia o argumento para o parâmetro em cada chamada. Para um primitivo, copia o valor; para um objeto, copia a referência. Não existe passagem por referência em Java, e essa regra única (explorada mais a fundo em Parâmetros de Método) explica três surpresas clássicas:
static void bumpPrimitive(int n) { n++; } // changes the copy only
static void mutate(StringBuilder sb) { sb.append("!"); } // edits shared object
static void rebind(StringBuilder sb) { // points the copy elsewhere
sb = new StringBuilder("new"); // caller's variable unchanged
}bumpPrimitive não pode afetar o chamador: recebeu uma cópia do número. mutate pode alterar o que o chamador vê, porque a referência copiada ainda aponta para o objeto do chamador no heap. rebind não pode, porque reatribuir o parâmetro apenas reconecta a cópia local da referência, não a variável do chamador.
O pool de strings, um caso especial do heap
Literais de string são internados: literais idênticos compartilham um objeto no pool de strings, então == (identidade de referência) retorna true para eles. Escrever new String("hi") força um objeto heap separado, então == retorna false mesmo que os caracteres sejam iguais. É por isso que você compara strings com .equals(), que verifica o conteúdo, não a identidade.
String a = "hi";
String b = "hi";
String c = new String("hi");
a == b; // true — both point at the pooled literal
a == c; // false — c is a distinct heap object
a.equals(c); // true — same charactersQuando as regiões se esgotam
Cada região tem um limite e seu próprio modo de falha. Recursão ilimitada continua empurrando frames até o stack ser esgotado, lançando StackOverflowError. Alocar objetos mais rápido do que o GC pode recuperá-los esgota o heap, lançando OutOfMemoryError. Ambos são Errors, não Exceptions — sinais de um problema estrutural (um caso base ausente, um vazamento) em vez de algo a ser capturado rotineiramente.
static int countDown(int n) {
return countDown(n - 1); // no base case -> StackOverflowError
}Um exemplo prático: observe as duas regiões se comportando
Este programa aborda todas as regras acima em uma única execução: um primitivo passado por valor, duas referências para um objeto no heap, uma mutação que o chamador vê, uma reatribuição que ele não vê, o pool de strings, um stack overflow deliberado e o descarte da última referência para um objeto.
O que extrair da execução:
scorepermanece10enquanto ondo método chega a110. O primitivo foi copiado para o novo frame, portanto nada que o método fizesse poderia alcançar de volta omain— isso é passagem por valor para primitivos, tornada visível.a.valueeb.valuesão ambos42ea == bétrue, porqueCounter b = acopiou uma referência, não o objeto. Uma instância no heap, duas variáveis de stack apontando para ela — edite por qualquer uma e ambas "enxergam".- Após
mutateThroughReference(a),a.valueé999. O método recebeu uma cópia da referência, mas a cópia ainda apontava para o mesmo objeto no heap, então a alteração do campo é visível ao chamador. - Após
reassignReference(a),a.valueainda é999, não-1. Reatribuir o parâmetro apenas reconectou a cópia local da referência do método; oado chamador nunca se moveu. Mutar o objeto funciona; reatribuir a variável não. lit1 == lit2étruemaslit1 == objéfalse, enquanto.equalsétruepara ambos. Literais internados compartilham um objeto no heap;new Stringforça um distinto. OStackOverflowErrorcapturado e otemp = nullfinal mostram os limites das duas regiões e como o heap se torna coletável quando sua última referência é descartada.
Prática
Capítulos relacionados
- Arquitetura da JVM — onde o stack e o heap ficam dentro do tempo de execução.
- Java Memory Model — como a memória se comporta entre threads.
- Garbage Collection — como os objetos inacessíveis no heap são recuperados.
- Parâmetros de Método — passagem por valor em profundidade.
- Referências — como as variáveis apontam para objetos no heap.
- O Pool de Strings — por que literais idênticos compartilham um único objeto.