Heap vs stack vs Metaspace: come funziona la JVM

La gestione della memoria di Java svolge un ruolo importante nell’eseguire in maniera efficiente le applicazioni Java.
Capirlo fino in fondo può aiutare chi sviluppa a evitare colli di bottiglia nelle prestazioni e in memory leak. Questo post ti introdurrà ai tre componenti principali del modello di memoria di Java: Heap, Stack e Metaspace.
Cosa vedrai
Heap
L’heap è un componente critico del modello di memoria di Java, perché funge da area primaria per l’archiviazione di oggetti Java.
Ogni volta che un oggetto viene creato utilizzando la parola chiave new
in Java, la memoria per quell’oggetto viene allocata dall’heap. È qui che opera anche il Garbage Collector, che recupera la memoria utilizzata dagli oggetti che non sono più necessari, il che aiuta a prevenire memory leaks e un utilizzo eccessivo della memoria.
Struttura dell’heap
L’heap Java è suddiviso in due aree principali:
Young Generation: è qui che vengono allocati tutti i nuovi oggetti. La Young Generation è ulteriormente divisa in uno spazio “Eden” e due spazi “Survivor”. Gli oggetti risiedono inizialmente in Eden e si spostano in uno spazio Survivor se rimangono in vita dopo un evento di “pulizia” da parte del Garbage Collector.
Old Generation: conosciuta anche come tenerured generation, riguarda gli oggetti che sono sopravvissuti a diversi cicli di garbage collection nella fase “young” e che vengono promossi in questo spazio, che è progettato per oggetti con un ciclo di vita più lungo. La soglia di sopravvivenza è impostata tramite le policy del garbage collector e può essere regolata da chi sviluppa.
Con Java 8 e versioni successive, l’introduzione di Metaspace ha sostituito Permanent Generation.
Esempio
public class HeapExample {
public static void main(String[] args) {
Customer customer = new Customer("John Doe");
System.out.println(customer.getName());
}
}
class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Nell’esempio precedente, l’oggetto Customer
, incluso il suo attributo name
, questo viene allocato nell’heap. La capacità dell’heap di allocare dinamicamente la memoria lo rende ideale per la gestione di oggetti complessi con durate di vita variabili.
Stack
Ogni thread in un’applicazione Java ha il suo stack, che viene utilizzato per memorizzare variabili di breve durata e informazioni sulle chiamate di metodo.
Lo stack è di dimensioni inferiori rispetto all’heap, ma è fondamentale per gestire le chiamate ai metodi e memorizzare variabili locali oppure i risultati intermedi delle espressioni.
Come funziona lo Stack
Quando viene invocato un nuovo metodo, sullo stack viene creato un nuovo blocco chiamato “stack frame”. Questo stack frame contiene tutte le variabili locali, i parametri e l’indirizzo di ritorno del metodo. Una volta che il metodo completa l’esecuzione, il suo stack frame viene scartato, rendendo quest’area altamente efficiente nella gestione della memoria che è necessaria solo durante una chiamata al metodo.
Esempio
public class StackExample {
public static void main(String[] args) {
int result = add(5, 10);
System.out.println("Result: " + result);
}
public static int add(int a, int b) {
int sum = a + b;
return sum; // The local variables 'a', 'b', and 'sum' are stored in the stack frame for the add method.
}
}
Questo esempio dimostra come lo stack viene utilizzato per gestire il flusso di esecuzione del metodo e il ciclo di vita delle variabili locali.
Metaspace
Metaspace è un’area di memoria non-heap che è nata con Java 8, in sostituzione di Permanent Generation. È utilizzata per archiviare metadati come definizioni di classe, metodi e campi.
A differenza dell’heap, Metaspace è allocato fuori dalla memoria nativa e la sua dimensione non è fissa ma può aumentare dinamicamente, il che aiuta a prevenire gli errori di tipo OutOfMemoryError
.
Monitoraggio e gestione del Metaspace
Poiché Metaspace può crescere dinamicamente, monitorarne le dimensioni e l’utilizzo è fondamentale per evitare che la memoria di sistema si esaurisca. Java fornisce diverse opzioni per monitorare e gestire le dimensioni di Metaspace, come -XX:MetaspaceSize
e -XX:MaxMetaspaceSize
, che consentono a chi sviluppa di impostare rispettivamente le dimensioni iniziali e massime di Metaspace.
Importanza del Metaspace
La sua introduzione migliora le prestazioni e la scalabilità regolando dinamicamente l’utilizzo della memoria in base alle richieste dell’applicazione, consentendo così alle applicazioni Java di gestire i metadati delle classi in modo più efficiente. Ciò è particolarmente utile in ambienti in cui molte classi vengono caricate e scaricate.
Schema della gestione degli spazi della JVM
Memoria nativa
Nello schema riportato sopra, viene mostrato un macro-gruppo chiamato memoria nativa: si tratta di un’area di memoria esterna al “normale” heap, ma in genere è una parte della memoria totale risparmiata dal sistema operativo per il processo JVM. Parte della memoria nativa è assegnata all’heap C, ossia lo spazio utilizzato dai programmi C nativi nei programmi Java, e parte allo stack.
Conclusione
Comprendere il modello di memoria di Java è fondamentale per ottimizzare le prestazioni delle applicazioni ed evitare problemi correlati alla memoria. Imparando a conoscere heap, stack e metaspace, puoi prendere decisioni più consapevoli quando sviluppi applicazioni Java. Monitora sempre l’utilizzo della memoria per assicurarti che le tue applicazioni funzionino in modo efficiente e per rilevare potenziali perdite di memoria!