Lo Stack e l'Heap in Java
Teoria
Durante l’esecuzione di un programma è la JVM che gestisce la memoria. Più precisamente essa viene divisa in due macro aree:
- Stack
- Heap
Le due aree hanno scopi e caratteristiche differenti.
Stack
All’interno dello stack vengono memorizzare tutte quelle informazioni che hanno uno scope di visibilità locale alla funzione e la cui dimensione non cambia durante il runtime. Ogni volta che viene chiamato un metodo si crea un nuovo record di attivazione. Dentro ad ogni record di attivazione vengono memorizzati i parametri, l’oggetto a cui appartiene (this), le variabili del metodo, il valore di ritorno e l’indirizzo della chiamata del metodo.
La dimensione dello stack viene gestita dalla JVM durante la compilazione e se viene superata si genera un errore di tipo StackOverflowError.
Lo stack si comporta come una pila, cioè, in modalità LIFO (Last Input First Output).
Heap
L’heap è l’area di memoria in cui sono memorizzati gli oggetti e i loro attributi.
Dato che gli oggetti sono memorizzati in modo sparso all’interno dell’heap, al momento della creazione di un nuovo oggetto, viene memorizzato nello stack l’indirizzo di memoria in cui si trova.
All’interno dell’heap gli oggetti sono memorizzati in modo non contiguo.
Per questo gestire in modo ottimale l'heap è un lavoro che viene affidato alla JVM che grazie al sistema "Garbage Collector" che permette di astrarre il problema della memoria al linguaggio di programmazione che utilizza diversi algoritmi per usare nel miglior modo lo spazio lasciato tra i vari oggetti.
Infatti è il garbage collector che pensa a gestire lo spazio e che libera gli oggetti che non verranno più utilizzati.
Gli elementi all’interno dell’heap sono accessibili in qualunque punto del programma.
Se la memoria all’interno dell’heap si riempie java genera l’errore java.lang.OutOfMemoryError.
L’heap è suddiviso in più parti:
- Young generation
- Survivor space
- Old generation
- Permanent generation
- Code Cache
Codice
Esempio Stack
Prendiamo come esempio per il funzionamento dello stack in questo programma:
public class Main {
public static void main(String[] args) {
int a = 5;
int b = 10;
}
}
Questo programma deve allocare 2 valori a 2 variabili e per farlo deve inserire questi valori da qualche parte nella memoria. Più precisamente saranno memorizzate all'interno dello stack.
Infatti con la parola riservata del java int diciamo alla JVM di riservare
nello stack 4 byte per ogni numero.
Alla fine del programma la memoria verrà liberata automaticamente dalla JVM.
Esempio con una funzione
Guardiamo step-by-step le righe del programma e osserviamo come cambia lo stack ad ogni passaggio.
public class Main {
public static void main(String[] args) {
int a = 5;
int b = 10;
foo();
}
public static void foo() {
int x = 123;
int y = 432;
}
}
Come prima cosa nello stack verranno inseriti i valori di a e di b.
Quindi ecco un diagramma dello stack:
| Record di attivazione | Indirizzo | Nome | Valore |
|---|---|---|---|
| main | 0x00000000 | a | 5 |
| 0x00000004 | b | 10 |
La chiamata del metodo foo() andrà a creare un nuovo record di attivazione
composto da:
xeyche sono le variabile che vengono inizializzate con i valori123e432;- l'indirizzo di memoria che punta al record di attivazione del metodo
main.
Di conseguenza lo stack sarà così:
| Record di attivazione | Indirizzo | Nome | Valore |
|---|---|---|---|
| main | 0x00000000 | a | 5 |
| 0x00000004 | b | 10 | |
| foo | 0x00000008 | x | 123 |
| 0x0000000C | y | 432 | |
| 0x00000010 | main | 0x00000000 |
Quando il metodo foo finisce nello stack il record di attivazione del metodo foo viene rimosso, cioè, si avrà questa situazione:
| Record di attivazione | Indirizzo | Nome | Valore |
|---|---|---|---|
| main | 0x00000000 | a | 5 |
| 0x00000004 | b | 10 |
Esempio Heap
Se vogliamo usare un oggetto dobbiamo affidarci all'heap.
In questo programma facciamo riferimento ad una classe Persona generica.
public class Main {
public static void main(String[] args) {
int a = 5;
Persona persona = new Persona("Mario","Rossi");
}
}
Quando il programma viene eseguito lo stack sarà in questo stato:
| Record di attivazione | Indirizzo | Nome | Valore |
|---|---|---|---|
| main | 0x00000000 | a | 5 |
| 0x00000004 | persona | indirizzo in cui è presente l'oggetto nell'heap |
Di solito l'indirizzo dell'heap parte dalla fine della RAM quindi ipotizzando di avere 1GB di RAM si
partirebbe dall'indirizzo 2^30-1 e scende verso 0x00000000.
Esempio con parametri e valore di ritorno
Quando passiamo un parametro ad una funzione, questo parametro viene copiato nella memoria dello stack.
Anche il valore di ritono viene salvato nello stack.
public class Main {
public static void main(String[] args) {
int a = 5;
int b = 10;
foo(a);
}
public static void foo(int x) {
int y = 123;
}
}
Quando viene chiamato il metodo foo() viene creato un nuovo record di
attivazione composto da:
xche è il parametro che viene inizializzato con il valore dia(5);- l'indirizzo di memoria che punta al record di attivazione del metodo
main.
Di conseguenza lo stack sarà così:
| Record di attivazione | Indirizzo | Nome | Valore |
|---|---|---|---|
| main | 0x00000000 | a | 5 |
| 0x00000004 | b | 10 | |
| foo | 0x00000008 | x | 5 |
| 0x0000000C | main | 0x00000000 |
Nella realtà
Nel caso in cui volessimo vedere lo stato dello stack e dell'heap di un nostro programma in java possiamo affidarci ai debugger integrati negli IDE più diffusi (Ecplipse, Intellij, Netbeans, Visual Studio, etc.).
Altrimenti possiamo usare da riga di comando i programmi inclusi nel JDK.
Con jps possiamo trovare il PID della JVM che sta
eseguendo il programma con il quale ci riferiremo di
seguito nei prossimi comandi.
Con jmap e jstack possiamo creare dei file dump nei
quali saranno presenti le varie informazioni sullo
stato dello stack e dell'heap.
Per visualizzare questi file possiamo usare il programma visualVM.
Esempio di jps:
$ jps
$ 5701 Jps
4460 Main
Esempio di jmap per visualizzare lo stato corrente della memoria:
$ jmap -histo:live <pid>
Esempio di jmap per salvare in un file binario lo stato corrente della memoria:
$ jmap -dump:format=b,file=heap.bin <pid>
Poi per visualizzare la memoria salvata in un file binario possiamo usare il programma visualVM.