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:

  • x e y che sono le variabile che vengono inizializzate con i valori 123 e 432;
  • 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:

  • x che è il parametro che viene inizializzato con il valore di a (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.