W3docs

Java Stack vs. Heap Memory

Come differiscono stack e heap in Java, cosa contiene ciascuno e il ciclo di vita di variabili e oggetti.

In fase di esecuzione la JVM divide la memoria che gestisce in due regioni con compiti molto diversi. Lo stack contiene la gestione delle chiamate a metodo — un frame per chiamata, con le variabili locali e i valori primitivi del metodo al suo interno. Lo heap contiene ogni oggetto creato con new, condiviso dall'intero programma e recuperato dal garbage collector. Quasi ogni comportamento Java fonte di confusione — perché un metodo "non può modificare" il tuo int, perché due variabili "vedono" la stessa modifica, perché la ricorsione profonda causa un crash — deriva direttamente da questa divisione.

Questo capitolo descrive cosa risiede in ciascuna regione, come differiscono i loro cicli di vita, perché la regola del passaggio per valore di Java deriva direttamente dalla divisione, il caso speciale del pool di stringhe, e cosa succede quando una delle due regioni si esaurisce. Per un quadro più ampio su come queste regioni si inseriscono nel runtime, consulta JVM Architecture e il Java Memory Model.

Due regioni, due cicli di vita

Lo stack è per thread e automatico: quando un metodo viene chiamato la JVM inserisce un frame, e quando il metodo ritorna quel frame viene rimosso e le sue variabili locali scompaiono istantaneamente. L'heap è condiviso e gestito: gli oggetti vivono finché nessun riferimento punta ad essi, dopodiché il garbage collector è libero di recuperare lo spazio. Nulla nell'heap scompare nel momento in cui un metodo ritorna.

AspettoStackHeap
ContieneFrame: variabili locali, primitivi, riferimentiOggetti, array, campi di istanza
AmbitoUno per threadUno condiviso dall'intera JVM
DurataFrame rimosso al ritorno del metodoFino a quando non raggiungibile, poi GC
AllocazionePush/pop, estremamente velocenew, gestita dall'allocatore
DimensionamentoLimitato (-Xss); l'overflow lancia StackOverflowErrorLimitato (-Xmx); l'esaurimento lancia OutOfMemoryError
PuliziaAutomatica, deterministicaGarbage collector, non deterministica

Cosa risiede esattamente dove

Una variabile locale risiede sempre nel frame dello stack corrente. Ciò che contiene dipende dal suo tipo. Per un primitivo, il frame contiene il valore stesso. Per un tipo oggetto, il frame contiene solo un riferimento — l'oggetto a cui punta risiede nell'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

I campi di istanza fanno parte dell'oggetto, quindi risiedono nell'heap insieme ad esso — anche un campo di tipo primitivo. Un private int balance all'interno di un oggetto Account è memoria dell'heap, non dello stack, perché appartiene all'oggetto, non a una singola chiamata a metodo.

Java utilizza il passaggio per valore — sempre

Java copia l'argomento nel parametro ad ogni chiamata. Per un primitivo, copia il valore; per un oggetto, copia il riferimento. Non esiste passaggio per riferimento in Java, e questa singola regola (approfondita in Method Parameters) spiega le tre classiche sorprese:

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 non può influenzare il chiamante: ha ricevuto una copia del numero. mutate può modificare ciò che il chiamante vede, perché il riferimento copiato punta ancora all'oggetto del chiamante nell'heap. rebind non può, perché riassegnare il parametro ricollega solo la copia locale del riferimento, non la variabile del chiamante.

Il pool di stringhe, un caso speciale dell'heap

I letterali stringa vengono internati: i letterali identici condividono un unico oggetto nel pool di stringhe, quindi == (identità di riferimento) restituisce true per essi. Scrivere new String("hi") forza un oggetto heap separato, quindi == restituisce false anche se i caratteri corrispondono. Questo è il motivo per cui si confrontano le stringhe con .equals(), che verifica il contenuto, non l'identità.

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 le regioni si esauriscono

Ciascuna regione ha un limite e la propria modalità di errore. La ricorsione illimitata continua ad inserire frame finché lo stack non si esaurisce, sollevando StackOverflowError. Allocare oggetti più velocemente di quanto il GC possa recuperarli esaurisce l'heap, sollevando OutOfMemoryError. Entrambi sono Error, non Exception — segnali di un problema strutturale (un caso base mancante, una perdita di memoria) piuttosto che qualcosa da gestire routinariamente.

static int countDown(int n) {
  return countDown(n - 1);   // no base case -> StackOverflowError
}

Un esempio pratico: osserva il comportamento delle due regioni

Questo programma tocca tutte le regole precedenti in un'unica esecuzione: un primitivo passato per valore, due riferimenti a un unico oggetto heap, una mutazione visibile al chiamante, una riassegnazione che non lo è, il pool di stringhe, un overflow dello stack deliberato, e l'eliminazione dell'ultimo riferimento a un oggetto.

java— editable, runs on the server

Cosa osservare dall'esecuzione:

  • score rimane 10 mentre il valore n del metodo raggiunge 110. Il primitivo è stato copiato nel nuovo frame, quindi nulla di ciò che il metodo ha fatto poteva raggiungere main — questo è il passaggio per valore per i primitivi, reso visibile.
  • a.value e b.value sono entrambi 42 e a == b è true, perché Counter b = a ha copiato un riferimento, non l'oggetto. Un'unica istanza nell'heap, due variabili nello stack che puntano ad essa — modificando attraverso una entrambe "vedono" la modifica.
  • Dopo mutateThroughReference(a), a.value è 999. Il metodo ha ricevuto una copia del riferimento, ma la copia puntava ancora allo stesso oggetto nell'heap, quindi la modifica del campo è visibile al chiamante.
  • Dopo reassignReference(a), a.value è ancora 999, non -1. Riassegnare il parametro ha ricollегato solo la copia locale del riferimento nel metodo; il valore a del chiamante non è mai cambiato. Mutare l'oggetto funziona; riassegnare la variabile non funziona.
  • lit1 == lit2 è true ma lit1 == obj è false, mentre .equals è true per entrambi. I letterali nel pool condividono un unico oggetto nell'heap; new String forza un oggetto distinto. Il StackOverflowError raggiunto ma gestibile e il temp = null finale mostrano i limiti delle due regioni e come l'heap diventa raccoglibile quando cade il suo ultimo riferimento.

Esercitazione

Pratica
Un metodo riceve un parametro di tipo StringBuilder. All'interno del metodo si chiama sb.append('x'). Dopo che il metodo ritorna, lo StringBuilder del chiamante mostra la 'x' aggiunta. Perché?
Un metodo riceve un parametro di tipo StringBuilder. All'interno del metodo si chiama sb.append('x'). Dopo che il metodo ritorna, lo StringBuilder del chiamante mostra la 'x' aggiunta. Perché?

Capitoli correlati

Was this page helpful?