JVM memory management
La gestione della memoria dei processi Java è affidata alla Java Virtual Machine. Essendo quest’ultima una macchina virtuale, virtualizza l’architettura del calcolatore di Von Neumann e gestisce le risorse di tale architettura. Una delle responsabilità chiave della JVM è gestire autonomamente i processi: viene pertanto fornita di una porzione di memoria dal sistema operativo.
Prima di analizzare nel dettaglio la gestione della RAM da parte della JVM, è importante avere chiaro il modello generale della memoria.
- È presente un’area statica di dimensione non dinamica (fissa) per contenuti determinati durante la scrittura del codice e pre-allocati in fase di compilazione;
- È presente uno Stack run-time di dimensione dinamica (variabile in esecuzione) per la gestione delle variabili e dei sottoprogrammi;
- È presente un supporto di dimensione dinamica (variabile in esecuzione) per la gestione degli oggetti chiamata Heap.
Oltre ad approfondire questi cenni, si teorizza la sitauzione presente in memoria e si analizza in via sperimentale la memoria del medesimo programma.
Area statica
La regione di memoria statica (conosciuta per essere l’area dei metodi) conserva i dati condivisi in tutti i thread del programma come ad esempio il bytecode delle classi, le variabili statiche, il codice dei metodi e le costanti determinabili a tempo di compilazione. Quest’area è collocata a parte e ha una dimensione fissa.
Questa entità ha un indirizzo assoluto e lo mantiene per tutta l’esecuzione del programma.
Area stack run-time
La regione di memoria dello stack (pila) memorizza le variabili locali e le chiamate dei metodi. Quando un metodo viene invocato, un nuovo frammento di stack viene allocato ad hoc in cima alla stessa pila.
Ogni frame dello stack (quindi ogni metodo allocato in memoria) contiene parametri, variabili locali e valore di ritorno.
La JVM alloca lo Stack durante l’esecuzione di dimensione variabile. Qualora lo stack si riempisse completamente (dato che il sistema operativo non ha dedicato abbastanza memoria alla JVM) verrebbe lanciata l’eccezione StackOverflowError (n.b.: per questo Java è poco prestante a metodi ricorsivi).
Area heap
Heap è la regione di memoria dove vengono allocati gli oggetti (tramite istruzione new). Questa parte di memoria viene gestita dalla JVM tramite il garbage collector dal momento che gli oggetti, per ovvie ragioni, non si comportano come visto precedente per i metodi (cfr.: programmazione procedurale). Nelle versioni di Java odierne, l’Heap si compone rispettivamente della young generation area e della old generation area (precedentemente vi era anche una permanent generation area ma è stata deprecata).
L’area dello Heap viene allocata durante l’esecuzione a dimensione variabile. Similmente all’area di Stack, qualora la memoria venisse riempita completamente, la JVM lancia l’eccezione OutOfMemoryError; tuttavia è possibile selezionare una massima memoria di Heap a piacere durante l’esecuzione del programma base aggiungendo la opzione -Xmx all’avvio del programma da terminale.
Come ogni sistema operativo, l’area di Heap è composta da algoritmi di frammentazione per la gestione degli indirizzi delle aree di memoria; questi ultimi, infatti, non sono allocati contiguamente come nel caso dello stack.
Analisi teorica della memoria della JVM
Consideriamo la seguente applicazione che crea e usa oggetti della classe Complesso:
1 2 3 4 5 6 7 8 9 10 | class ProvaComplesso { public static void main(String[] args) { Complesso x, y; x = new Complesso(3.0, 4.0); // main, 1 y = new Complesso(-3.0, 2.0); // main, 2 x.sommati(y); // main, 3 } } |
Vogliamo descrivere l’esecuzione dell’applicazione ProvaComplesso concentrandoci sulla allocazione e deallocazione di oggetti nello heap, heap mostrando anche la pila di attivazione.
Ecco il codice del costruttore e del metodo sommati della classe Complesso:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Complesso { public Complesso(double re, double im) { this.re = re; this.im = im; } public void sommati(Complesso altro) { this.re += altro.re; this.im += altro.im; } } |
Il diagramma di sequenza per ProvaComplesso è il seguente.
Tracing dell'esecuzione:
- Nell’istante t=0 la JVM carica in memoria la classe ProvaComplesso, allocando un’area nello heap
- Nessun metodo è in esecuzione
- Nessun oggetto è stato creato
- In t=1 viene avviata l’esecuzione della classe ProvaComplesso, invocandone il metodo di classe main
- Vengono allocate le variabili locali x e y di main — di tipo Complesso
- Essendo stata chiamata in causa, viene allocata un’area di memoria per la classe Complesso
- In t=2 viene eseguita l’istruzione x = new Complesso(3.0, 4.0)
- Viene creato un primo oggetto Complesso - c1
- La creazione di c1 richiede l’allocazione di un’area di memoria nello heap per memorizzarne, tra l’altro, le variabili d’istanza re e im
- Viene invocato il costruttore di Complesso
- Viene creato un record di attivazione per il costruttore
- In t=3, dopo che è stato eseguito il corpo del costruttore, il costruttore viene rimosso dalla pila di attivazione
- Viene poi eseguita l’assegnazione a x
- In t=4 viene eseguita l’istruzione y = new Complesso(-3.0, 2.0)
- Viene creato un altro oggetto Complesso - c2 e allocata un’area di memoria nello heap per c2
- Viene invocato il costruttore di Complesso e creato un record di attivazione per il costruttore
- In t=5, dopo che il costruttore è stato eseguito, il suo record di attivazione viene rimosso dalla pila
- Viene poi eseguita l’assegnazione a y
- In t=6 viene eseguita l’istruzione x.sommati(y)
- Viene creato un record di attivazione per sommati
- In t=7, dopo che il metodo sommati è stato eseguito, il suo record viene rimosso dalla pila di attivazione
- Osserva che l’esecuzione di sommati ha modificato lo stato dell’oggetto c1
- In t=8 il metodo main termina ed il suo record è rimosso dalla pila di attivazione
- Non essendo referenziati altrove, gli oggetti c1 e c2 verranno deallocati
Analisi laboratoriale della memoria della JVM (jmap)
Per analizzare quanto vi sia instanziato nella memoria di una JVM su una distribuzione di linux è necessario:
- Scansionare i processi in esecuzione con java tramite il programma jps e ricavare il PID del processo da analizzare
- Vedere quanto è stato allocato in memoria tramite il programma jmap
matteo@localhost:~$jps -l
matteo@localhost:~$jmap -histo <PID>
Ho scansionato la memoria del programma precedentemente analizzato teoricamente. Risultati raw: pdf, ods, csv. A seguire un estratto.
ID | N° istanze | N° bytes | Classe |
109: | 2 | 64 | complesso.Complesso |
Analizzando la riga 109 possiamo riconoscere che:
- Effettivamente sono state allocati 2 oggetti della classe Complesso.
- Effettivamente le istanze della classe Complesso sono di 64 bytes (4 variabili di tipo double ciascuna).
Grazie ai risultati ottenuti, posso affermare che:
- Come ogni altro sistema operativo, la JVM esegue ed alloca processi senza che io (utente ma anche programmatore) ne sia consapevole.
- L'alto livello del linguaggio Java comporta astrazioni durante la programmazione e questo facilita il lavoro del programmatore, ma questo comporta conseguenze in termini di efficienza.
- La gerarchizzazione delle classi e del codice facilita il lavoro del programmatore ma comporta requisiti funzionali maggiori.