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.
| Aspetto | Stack | Heap |
|---|---|---|
| Contiene | Frame: variabili locali, primitivi, riferimenti | Oggetti, array, campi di istanza |
| Ambito | Uno per thread | Uno condiviso dall'intera JVM |
| Durata | Frame rimosso al ritorno del metodo | Fino a quando non raggiungibile, poi GC |
| Allocazione | Push/pop, estremamente veloce | new, gestita dall'allocatore |
| Dimensionamento | Limitato (-Xss); l'overflow lancia StackOverflowError | Limitato (-Xmx); l'esaurimento lancia OutOfMemoryError |
| Pulizia | Automatica, deterministica | Garbage 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 GCI 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 charactersQuando 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.
Cosa osservare dall'esecuzione:
scorerimane10mentre il valorendel metodo raggiunge110. Il primitivo è stato copiato nel nuovo frame, quindi nulla di ciò che il metodo ha fatto poteva raggiungeremain— questo è il passaggio per valore per i primitivi, reso visibile.a.valueeb.valuesono entrambi42ea == bètrue, perchéCounter b = aha 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è ancora999, non-1. Riassegnare il parametro ha ricollегato solo la copia locale del riferimento nel metodo; il valoreadel chiamante non è mai cambiato. Mutare l'oggetto funziona; riassegnare la variabile non funziona. lit1 == lit2ètruemalit1 == objèfalse, mentre.equalsètrueper entrambi. I letterali nel pool condividono un unico oggetto nell'heap;new Stringforza un oggetto distinto. IlStackOverflowErrorraggiunto ma gestibile e iltemp = nullfinale mostrano i limiti delle due regioni e come l'heap diventa raccoglibile quando cade il suo ultimo riferimento.
Esercitazione
Capitoli correlati
- JVM Architecture — dove stack e heap si trovano all'interno del runtime.
- Java Memory Model — come la memoria si comporta tra i thread.
- Garbage Collection — come vengono recuperati gli oggetti heap non raggiungibili.
- Method Parameters — il passaggio per valore in dettaglio.
- References — come le variabili puntano agli oggetti nell'heap.
- The String Pool — perché i letterali identici condividono un unico oggetto.