È possibile identificare quattro categorie di problematiche che affliggono la memoria, i cui effetti risultano simili, ma le cui cause e soluzioni sono diverse.
- Performance: di norma derivante da un volume eccessivo di creazioni e cancellazioni di oggetti, lunghi periodi di attesa prima che l’oggetto allocato possa essere deallocato dal GC, operazioni di swapping da parte del sistema operativo e così via;
- Vincoli sulle risorse: in caso di eccessiva frammentazione della memoria o quando la memoria disponibile è troppo piccola, può presentarsi l’impossibilità di allocare un oggetto probabilmente di dimensioni considerevoli, sia nella memoria nativa che nel Java heap. Analogo discorso per i metadati relativi alle classi utilizzate che al crescere del volume di classi caricate possono saturare lo spazio loro destinato dalla JVM;
- Java heap leaks: è probabilmente il principale memory leak che affligge i programmi Java e deriva spesso da riferimenti ancora attivi ad oggetti non più utilizzati;
- Native memory leaks: memory leaks derivanti dalla crescita di occupazione della memoria nativa;
I memory leaks in definitiva possono degradare le prestazioni di un’applicazione, arrivando anche a saturarne la memoria, con conseguente rischio di arresto o comunque di perdita delle funzionalità.
Il sintomo più evidente che si può ottenere a runtime è un errore del tipo OOM, OutOfMemoryError
, errore che però può avere diverse cause non necessariamente connesse con un memory leak del programma. In questa guida ci concentreremo sull’analisi e risoluzione dei problemi connessi con lo Java heap.
OutOfMemoryError
Errori OOM non necessariamente implicano memory leaks, ad esempio potrebbero essere connessi con il consumo eccessivo di memoria da parte di un altro processo, o dovuti a una generazione eccessiva di variabili locali e così via. Inoltre, non tutti i leaks si manifestano con un OOM, in modo particolare su applicazioni che restano attive per lunghi periodi di tempo senza riavvii.
Nel caso ci si imbatta in un OOM occorre determinarne il significato, non sempre evidente. Seguono alcuni dei possibili messaggi d’errore:
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?
java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)
Nel caso del Java heap space partiamo con il dire che ciò non implica necessariamente un memory leak. Il messaggio indica che lo spazio messo a disposizione dell’heap non è risultato sufficiente, ad esempio a causa dell’ingresso in memoria di un oggetto troppo voluminoso, o per carichi particolarmente elevati. Ciò semplicemente può essere risolto aumentando lo spazio a disposizione dell’heap (agendo sui parametri della JVM) in modo che sia in grado di servire anche oggetti più voluminosi (o eventualmente un certo numero di oggetti voluminosi in parallelo).
In altri casi, e ciò si evidenzia nei programmi che rimangono in attività per lunghi periodi di tempo, il messaggio probabilmente indica che si stanno mantenendo in memoria riferimenti a oggetti non più utilizzati, riferimenti che pur inutili impediscono al Garbage Collector di liberare la relativa memoria. Questo è il classico memory leak in Java.
Altra potenziale causa di questo genere di errori è connesso con l’uso del finalizer. Gli oggetti di classi che implementano questo metodo non vedono liberare immediatamente il relativo spazio dal Garbage Collector. Dopo che gli oggetti vengono individuati dal GC come inutilizzati, invece di rilasciare subito la memoria, vengono schedulati in una coda per la finalizzazione, finalizzazione che avviene in un secondo momento. Il tempo in cui avviene la finalizzazione è a discrezione dell’implementazione della JVM.
Ad esempio, nel caso dell’implementazione Sun della JVM, la finalizzazione avviene per mezzo di un daemon thread (threads che hanno la caratteristica di non mantenere attiva la JVM qualora tutti gli altri threads terminino l’esecuzione). Se questo thread non riesce a tenere il passo con la coda di finalizzazione (ossia entrano in coda più oggetti di quanti il thread riesce a finalizzare), la coda potrebbe saturare l'heap e portare ad avere un OOM.
Nel caso di PermGen space
è lo spazio permanent generation a essere saturo. si tratta dello spazio destinato a raccogliere le meta-informazioni relative alla classi. E’ uno spazio di dimensioni prefissate e nel caso di caricamento di un numero massivo di classi può saturarsi, richiedendo pertanto più risorse di quelle previste al lancio dell’applicazione (di norma un valore di default). E’ possibile risolvere questo problema aumentando lo spazio destinato a questa area della memoria Java (opzione -XX:MaxPermSize
). Inoltre, oggetti Interned java.lang.String
vengono memorizzati in questa area, pertanto applicazioni che internano un elevato numero di stringhe possono portare a saturare la permanent generation.
Il problema è stato affrontato nel design della Java 8, nella quale la collezione dei metadati è stata rivista e la permanent generation sostituita da una nuova area di memoria, metaspace memory, che di default non ha una dimensione prefissata e può ingrandirsi dinamicamente, evitando di incappare nei classici errori che affliggevano la PermGen Area.
In Requested array size exceeds VM limit
il messaggio indica che si sta provando ad allocare un array più largo delle dimensioni dell’heap. Spesso in questo caso il problema è risolvibile incrementando l’area a disposizione dell’heap o verificando la presenza di un eventuale bug.
request <size> bytes for <reason>. Out of swap space?
è un’eccezione che viene sollevata quando un’allocazione nell’heap nativo fallisce e può essere vicino all’esaurimento. Di norma questa eccezione capita quando il sistema operativo è configurato con uno spazio di swap insufficiente o un altro processo del sistema sta consumando tutte le risorse della memoria, o ancora quando l’applicazione va in errore a causa di un leak nativo.
<reason> <stack trace> (Native method)
è un messaggio accompagnato da un metodo nativo in cima allo stack trace
indica che un metodo nativo ha fallito un’allocazione. Al contrario del messaggio precedente, il fallimento nell’allocazione è stato individuato nel metodo nativo o in un contesto JNI.
Organizzazione della memoria in Java e Garbage Collector
Prima di proseguire, diamo un’occhiata a come lavora il Garbage Collector e come è strutturata la memoria nella JVM.
La memoria nella JVM si può dividere in heap e non heap. Come visto finora, la memoria non heap ospita le meta-informazioni relative alle classi ed è comunemente definita Permanent Generation area. In Java 8 è stata modificata in modo da ridurre la necessità di agire sui parametri della JVM (ed evitare PermGen fault
).
L’area heap è quella deputata a collezionare gli oggetti creati durante l’esecuzione dell’applicazione ed è divisibile in due macro aree: Young Generation e Tenured Generation (anche detta "old").
L’area della Young Generation è a sua volta divisa in Eden Space e Survivor Space.
Gli oggetti creati inizialmente trovano posto nell’Eden Space, area servita da un Garbage Collector ottimizzato (Minor GC). Quando il Minor GC analizza l’area, rimuove i riferimenti ad oggetti non più utilizzati liberando la relativa area occupata, facendo migrare gli oggetti con riferimenti ancora attivi nella Survivor Space area. Nel Survivor Space (in diverse implementazioni a sua volta diviso in due aree distinte) il Garbage Collector opera con una frequenza inferiore e gli oggetti che sono ancora attivi vengono trasferiti nella Tenured Generation, per rimanervi finché non saranno distrutti dal Garbage Collector.
Quando quest'ultimo spazio sta per riempirsi viene eseguito un Major GC (anche detto "Full GC"), un ciclo di Garbage Collector computazionalmente più oneroso. E’ dunque nella tenured generation che possiamo cercare i sintomi di un memory leak in quanto è qui che si accumulano gli oggetti che andrebbero de-allocati ma che mantengono riferimenti attivi. Vedremo che i cicli del Garbage Collector non riusciranno a deallocare spazio in questa area, o meglio che lo spazio ancora allocato dopo ogni ciclo di Garbage Collector tenderà costantemente a crescere.
È possibile avere dei riscontri a video da parte del Garbage Collector abilitando la verbosità tramite i parametri della JVM (-verbosegc
).