/ Cambia tema
Versione stampabile
Bibliografia
Contatti

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.

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:

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. In t=6 viene eseguita l’istruzione x.sommati(y)
    • Viene creato un record di attivazione per sommati
  8. 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
  9. 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:

  1. Scansionare i processi in esecuzione con java tramite il programma jps e ricavare il PID del processo da analizzare
  2. matteo@localhost:~$jps -l
  3. Vedere quanto è stato allocato in memoria tramite il programma jmap
  4. matteo@localhost:~$jmap -histo <PID>

Ho scansionato la memoria del programma precedentemente analizzato teoricamente. Risultati raw: pdf, ods, csv. A seguire un estratto.

IDN° istanzeN° bytesClasse
109:264complesso.Complesso

Analizzando la riga 109 possiamo riconoscere che:

Grazie ai risultati ottenuti, posso affermare che: