Questo articolo introduce i principali parametri che condizionano il comportamento del Garbage Collector e che permettono l‘ottimizzazione dell‘uso della memoria. Sono inoltre introdotti gli strumenti di monitoraggio che consentono di verificare le performance del GC e l‘efficienza dell‘utilizzo della heap memory.
La serie dedicata al Garbage Collector ha finora mostrato i concetti di base relativi al riciclo degli oggetti in memoria e agli algoritmi utilizzati dalle moderne Java Virtual Machine per il garbaging delle risorse inutilizzate.
Una volta chiariti i meccanismi che regolano il Garbage Collector è possibile ottimizzarne sia il comportamento che le prestazioni. Il primo obiettivo da perseguire è sicuramente quello di scegliere l’adeguato dimensionamento della heap memory e i parametri corretti del Garbage Collector allo scopo di evitare sia sprechi di risorse che eccezioni di OutOfMemory, che sono una delle cause più frequenti di instabilità di una JVM. Ottenuta una buona stabilità, si può procedere con l’ottimizzazione delle performance in funzione delle metriche ritenute più rilevanti, le più importanti delle quali sono generalmente considerate il throughput e il low pause. Qualsiasi ottimizzazione apportata al comportamento del Garbage Collector va comunque costantemente monitorata per verificare di aver ottenuto i risultati sperati. Questo articolo descrive gli strumenti messi a disposizione dalla JVM per il monitoraggio dell’allocazione delle risorse a runtime e per l’analisi a posteriori delle condizioni della heap memory in un dato momento o a seguito dell’esaurimento dello spazio in memoria.
Configurazione e ottimizzazione del Garbage Collector
La Java Virtual Machine mette a disposizione dell’amministratore diverse opzioni a riga di comando che influenzano molti aspetti del comportamento del Garbage Collector, come il tipo di algoritmo da utilizzare o il dimensionamento delle generazioni. Se non specificato diversamente, nel presente articolo si fa riferimento alla versione 1.6 della JVM, anche se il significato di quasi tutte le opzioni è valido anche nella versione 1.5; la precedente versione 1.4, invece, limitava maggiormente le possibilità di configurazione del GC.
Il primo parametro preso in considerazione in fase di configurazione del garbaging delle risorse è generalmente la dimensione massima della heap memory (-Xmx), che riveste particolare importanza perche’ una heap memory troppo piccola può provocare frequenti OutOfMemory e il crash della JVM, mentre una heap memory troppo grande porta a un utilizzo inefficiente delle risorse e a pause di garbaging lunghe visto che il riciclo di oggetti può riguardare sezioni di memoria potenzialmente molto grandi. È anche possibile definire la dimensione minima della heap memory (-Xms), che la JVM istanzia in fase di inizializzazione. Durante l’esecuzione il GC adegua le dimensioni delle varie generazioni (Eden, Survivor, Tunered e Permanent) in modo da soddisfare le richieste di allocazione e da mantenere la heap memory nei limiti indicati. Definire la dimensione minima e massima uguali costringe il GC a fissare la dimensione delle generazioni a quella stabilita inizialmente senza la possibilità di adeguarle alle esigenze di allocazione e tale vincolo può provocare un abbassamento del throughput e la potenziale saturazione di una generazione se inizialmente mal dimensionata.
I parametri di dimensionamento della heap memory sono sicuramente i più importanti e i più usati (e abusati), ma la JVM mette a disposizione anche altre importanti opzioni per controllare il comportamento del Garbarge Collector, che sono descritte di seguito con i relativi valori di default
-XX:MinHeapFreeRatio=n
-XX:MaxHeapFreeRatio=m
Indicano percentualmente la dimensione minima e massima di spazio libero per ogni generazione. Quando lo spazio libero scende sotto la percentuale minima, il Garbage Collector aumenta la dimensione della generazione in maniera da far aumentare la quota di spazio libero, compatibilmente con la dimensione massima della heap memory. Analogamente il GC si comporta quando lo spazio libero sfora la percentuale massima. Questo meccanismo permette di avere sempre generazioni dimensionate in maniera adeguata, minimizzando lo spreco di risorse. Modificare i valori di default può significare aumentare i benefici di questo meccanismo, ma anche costringere il GC a modificare continuamente le dimensioni delle generazioni diminuendo il throughput o al contrario portare ad un cattivo utilizzo di risorse. I valori di default sono rispettivamente 40 e 70.
-XX:NewSize=n
Indica la dimensione iniziale della young generation. Effettuare un tuning corretto di tale parametro può portare a tempi di startup più veloci, perche’ il GC non perde tempo nell’adeguare le dimensioni della young generation durante l’inizializzazione dell’applicativo. Il valore di default dipende dalla piattaforma su cui gira la JVM.
-XX:NewRatio=n
Indica il rapporto tra le dimensioni della young e della old generation. Se, come accade di default per JVM client class, n=2 significa che il GC tenderà a porre la dimensione della young generation in rapporto 1:2, ovvero la young generation sarà un terzo dell’intera dimensione della heap memory. Per JVM server class il valore di default è pari a 8.
-XX:SurvivorRatio=n
Indica il rapporto tra ogni semispazio nel survivor space. Se ad esempio n=7, ogni semispazio è un nono dell’intera young generation (non un ottavo perche’ i semispazi sono due). Il valore di default è 32.
-XX:MaxPermSize=n
Indica la dimensione massima della permanent generation e il suo valore di default dipende dalla piattaforma su cui gira la JVM.
Altre importanti opzioni si possono definire in funzione del tipo di Garbage Collector che si intende utilizzare. Se si sceglie di usare il Parallel Collector (-XX:+UseParallelGC) o il Parallel Compacting Collector (-XX:UseParallelOldGC) si possono definire il numero di thread dedicati alle fasi concorrenti del garbaging, con l’opzione -XX:ParallelGCThreads=n, e gli obiettivi di low pause e throughput con le opzioni -XXMaxGCPauseMillis=n e -XXGCTimeRatio, descritti nel precedente articolo. Utilizzando il CMS Collector si hanno a disposizione, oltre che l’opzione per stabilire il numero di thread da dedicare al garbaging, le opzioni -XX:+CMSIncrementalMode e -XX:+CMSIncrementalPacing. La prima abilita la modalità incrementale della fase concorrente, per cui periodicamente tale fase viene fermata per ritornare all’esecuzione dell’applicativo. La seconda abilita il controllo automatico dell’ammontare di lavoro che la fase concorrente può eseguire prima di dover ritornare il controllo all’esecuzione applicativa.
Monitoraggio del Garbage Collector e della Heap Memory
La scelta dei valori delle opzioni mostrate finora permettono l’ottimizzazione dell’uso della memoria della JVM e la minimizzazione del tempo dedicato al garbaging. La corretta valorizzazione di questi parametri, in funzione delle metriche di performance che si voglio ottimizzare, può dipendere da molti fattori, quali le risorse hardware disponibili, il modo in cui gli applicativi sfruttano le risorse, il numero di utenti che utilizzano gli applicati etc… Queste condizioni possono variare frequentemente nel tempo, ad esempio a seguito di un upgrade hardware, all’aggiornamento del software o all’aumento sostanziale del numero di utenti. L’ottimizzazione del comportamento del GC deve essere quindi considerato un processo ricorrente nel tempo ed è importante che sia accompagnato da un monitoraggio continuo della variazione dell’uso delle risorse di memoria.
La Java Virtual Machine mette a dispozione dell’amministratore di sistema una serie di strumenti dedicati al monitoraggio dell’uso della heap memory e del comportamento del Garbage Collector. Per il monitoraggio continuo e prolungato nel tempo si possono usare l’opzione -verbose:gc o il tool jstat. Nel primo caso si abilita il logging del Garbage Collector, che può essere ulteriormente esteso con le opzioni -XX:+PrintGCDetails e -XX:+PrintGCTimeStamps e rediretto verso un file con il parametro -Xloggc:. In questo modo, ogni volta che esegue una minor o una major collection, il Garbage Collector stampa diverse informazioni sulle condizioni della heap memory e sui tempi di esecuzione del garbaging. Il tool jstat è, invece, un tool di monitoraggio molto simile al forse più conosciuto vmstat, usato per il monitoraggio di sistemi UNIX/Linux. Jstat scrive, con la frequenza passata come parametro, una riga che mostra informazioni relative al componente scelto. Ad esempio con l’opzione -gcutil stampa informazioni relative alle percentuali di utilizzo dei due semispazi e delle altre generazioni, il numero di minor e major collection eseguite dall’avvio della JVM e il tempo impiegato ad eseguirle. Le altre opzioni a disposizione (-gccause, -gcnew, -gcold etc.) permettono di monitorare praticamente ogni aspetto relativo all’uso della memoria e non solo. In caso di scarsa stabilità di una JVM, l’uso di questi due strumenti può essere utile per stabilire le condizioni della heap memory al momento del crash e per verificare se l’uso inefficiente o la scarsità delle risorse di memoria ne sono la causa.
La JVM mette anche a disposizione uno strumento di monitoraggio della gestione della memoria, la JConsole, che permette la visualizzazione grafica dell’andamento delle heap memory e delle varie generazioni, oltre che al monitoriaggio dei thread e delle classi istanziate. Di seguito è mostrato il grafico dell’andamento di una heap memory prodotto con JConsole
Figura 1 – Monitoraggio della heap memory con la JConsole
In particolare in figura 1 è mostrato un JMeter con qualche problema di allocazione di memoria e con la Tunered Generation completamente occupata e in procinto di provocare un OutOfMemory. La JConsole, essendo uno strumento grafico, si presta meno al monitoraggio continuo della memoria, ma può essere utile per la produzione di documentazione comprensibile anche a personale non tecnico.
Generazione e analisi di un Memory Heap Dump
Gli strumenti di monitoraggio visti finora permettono di aver informazioni aggregate sullo stato della heap memory e sulle dimensioni delle varie generazioni. Ci sono casi in cui è interessante conoscere anche quali sono le classi le cui istanze occupano le risorse di memoria, per capire ad esempio se siamo in presenza di memory leak, che non possono essere completamente esclusi neanche in linguaggi che usano il garbaging automatico delle risorse come Java. Allo scopo di avere dettagli sugli oggetti che sono istanziati a runtime la JVM mette a disposizione i tool JMap e JHat. Il primo può essere usato con l’opzione -dump per indurre una JVM in esecuzione a produrre un memory heap dump, cioè un file binario contenente una “fotografia” della heap memory in un dato istante. Il tool jhat permette di analizzare un heap dump attraverso un’interfaccia web che consente di ottenere il numero di istanze per ogni classe con il relativo spazio occupato in memoria, l’insieme degli oggetti radice da cui inizia il garbaging delle risorse e altre informazioni. L’opzione -XX:+HeapDumpOnOutOfMemoryError induce la JVM a creare un file di heap dump a seguito di un evento di OutOfMemory, allo scopo di avere l’esatta situazione della memoria al momento del verificarsi dell’eccezione e di poterla successivamente analizzarla con JHat.
Analisi e monitoraggio con VisualVM
Molte delle possibilità offerte dagli strumenti di monitoraggio e analisi descritti finora sono fornite a partire dalla versione 1.6 della JVM da un unico tool di monitoraggio. Questo strumento, chiamato VisualVM, permette il monitoraggio del comportamento a runtime di una JVM, e sia la produzione che l’analisi di memory heap dump; inoltre, la sua struttura a plugin permette l’estendibilità delle sue funzioni. In particolare il plugin VisualGC è interessante per quanto trattato in questo articolo perche’ permette di visualizzare chiaramente l’andamento di tutte le generazioni in cui il Garbage Collector divide la heap memory e per ognuna di esse fornisce la dimensione attuale, la percentuale di occupazione e il numero di collection effettuate. In figura 2 è mostrato un esempio di monitoraggio della JVM di un application server fatto con il plugin VisualGC di VisualVM
Figura 2 – Monitoraggio della heap memory con il plugin VisualGC di VisualVM
Conclusioni
Le precedenti parti hanno mostrato i parametri e gli strumenti che la JVM mette a disposizione per ottimizzare e il monitorare le risorse di memoria e il comportamento del Garbage Collector. L’ottimizzazione e il monitoraggio sono due attività strettamente legate in qualsiasi sistema hardware o software e che non possono prescindere dalla comprensione del sistema stesso. Questo articolo conclude la serie dedicata al Garbage Collector che ha cercato di introdurre i concetti di base relativi al garbaging delle risorse, di descrivere in che modo le moderne Java Virtual Machine organizzano la heap memory, fino ad arrivare a mostrare le principali metodologie e gli strumenti di ottimizzazione e monitoraggio delle performance.
La garbage collection è comunque un’area di studio molto vasta e, per chi volesse approfondirla nei minimi dettagli, si rimanda a “the Garbage Collection Page”[5] che raccoglie una bibliografia di quasi 1900 libri e articoli sull’argomento.
Riferimenti
[1] Memory Management in the Java HotSpot™ Virtual Machine
http://java.sun.com/j2se/reference/whitepapers/memorymanagement_whitepaper.pdf
[2] JVMSTAT 3.0
http://java.sun.com/performance/jvmstat/
[3] Using JConsole to monitor applications
http://java.sun.com/developer/technicalArticles/J2SE/jconsole.html
[4] VisualVM
https://visualvm.dev.java.net/
[5] the Garbage Collection Page
http://www.cs.kent.ac.uk/people/staff/rej/gc.html