W3docs

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.

AspectoStackHeap
ArmazenaFrames: locais, primitivos, referênciasObjetos, arrays, campos de instância
EscopoUm por threadUm compartilhado por toda a JVM
Tempo de vidaFrame removido no retorno do métodoAté ficar inacessível, então GC
AlocaçãoPush/pop, extremamente rápidonew, gerenciado pelo alocador
DimensionamentoLimitado (-Xss); overflow lança StackOverflowErrorLimitado (-Xmx); esgotamento lança OutOfMemoryError
LimpezaAutomática, determinísticaGarbage 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 GC

Os 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 characters

Quando 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.

java— editable, runs on the server

O que extrair da execução:

  • score permanece 10 enquanto o n do método chega a 110. O primitivo foi copiado para o novo frame, portanto nada que o método fizesse poderia alcançar de volta o main — isso é passagem por valor para primitivos, tornada visível.
  • a.value e b.value são ambos 42 e a == b é true, porque Counter b = a copiou 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.value ainda é 999, não -1. Reatribuir o parâmetro apenas reconectou a cópia local da referência do método; o a do chamador nunca se moveu. Mutar o objeto funciona; reatribuir a variável não.
  • lit1 == lit2 é true mas lit1 == obj é false, enquanto .equals é true para ambos. Literais internados compartilham um objeto no heap; new String força um distinto. O StackOverflowError capturado e o temp = null final mostram os limites das duas regiões e como o heap se torna coletável quando sua última referência é descartada.

Prática

Prática
Um método recebe um parâmetro do tipo StringBuilder. Dentro do método você chama sb.append('x'). Após o retorno do método, o StringBuilder do chamador mostra o 'x' acrescentado. Por quê?
Um método recebe um parâmetro do tipo StringBuilder. Dentro do método você chama sb.append('x'). Após o retorno do método, o StringBuilder do chamador mostra o 'x' acrescentado. Por quê?

Capítulos relacionados

Was this page helpful?