mar 19 marzo 2024 - Lo Sviluppatore  anno VI

Java: La Garbage Collection

Java Garbage Collection
Condividi

La gestione della memoria di Java, con la sua garbage collection integrata, è uno dei migliori risultati ottenuti nello sviluppo di questo linguaggio. Questa caratteristica consente agli sviluppatori di creare nuovi oggetti senza preoccuparsi esplicitamente dell’allocazione e deallocazione della memoria, in quanto è il garbage collector che recupera automaticamente la memoria per il riutilizzo sgravando il programmatore dal doversi occupare dei problemi legati alla gestione della memoria.

Il Garbage Collector è un thread Daemon che sta in esecuzione in background. Fondamentalmente, libera la memoria heap distruggendo gli oggetti non più raggiungibili. Gli oggetti non raggiungibili sono quelli a cui non fa più riferimento alcuna parte del programma. Ma vediamo più in dettaglio come funziona

Funzionamento del Garbage Collector

La Garbage collection automatica è un processo di osservazione della memoria Heap, che identifica o meglio “marchia” (si parla infatti di marking process) gli oggetti irraggiungibili li distrugge e compatta la memoria liberata per un uso successivo più efficiente. Nelle figure sotto il processo appena descritto:

Il primo passo nel processo è chiamato marcatura (marking). Qui il garbage collector identifica quali pezzi di memoria sono in uso e quali no.

Java: La Garbage Collection 1

Gli oggetti riferiti sono mostrati in blu. Gli oggetti senza riferimento sono mostrati in giallo. Tutti gli oggetti vengono scansionati nella fase di marcatura per effettuare questa discriminazione. Questo può essere un processo che richiede molto tempo se tutti gli oggetti in un sistema devono essere sottoposti a scansione.

Il secondo passo è la cancellazione normale dove vengono rimossi gli oggetti senza riferimento e lasciati gli oggetti referenziati e i puntatori allo spazio libero. L’allocatore di memoria contiene i riferimenti ai blocchi di spazio libero in cui è possibile allocare nuovi oggetti.

Java: La Garbage Collection 2

A completare il passo 2 e per migliorare ulteriormente le prestazioni, oltre all’eliminazione di oggetti senza riferimento, è possibile compattare anche gli oggetti riferiti rimanenti. Spostando insieme gli oggetti referenziati rende le nuove allocazioni di memoria molto più semplici e veloci.

Java: La Garbage Collection 3

Da analisi empiriche delle applicazioni si è visto, comunque che la maggior parte degli oggetti ha vita breve e questa cosa è stata sfruttata per migliorare le prestazioni della JVM creando una metodologia denominata Garbage Collection Generazionale. Con questo metodo, lo spazio di heap è diviso in generazioni: Generazione giovane (Young Generation), Generazione vecchia o di ruolo (Old or Tenured Generation) e Generazione permanente (Permanent Generation), quest’ultima presente fino a Java 7.Un problema di questo approccio è che, man mano che aumenta il numero di oggetti, il tempo di Garbage Collection continua ad aumentare, poiché deve passare attraverso l’intero elenco di oggetti, cercando gli oggetti irraggiungibili.

Java: La Garbage Collection 4

Lo spazio heap della Young Generation è dove vengono allocati tutti i nuovi oggetti creati; una volta che questo spazio viene riempito, si attiva quella che si chiama una minor garbage collection (nota anche come GC secondario) che distrugge tutti gli oggetti “morti” (non più riferiti) di questa generazione. A seguito dell’alto “tasso di mortalità” degli oggetti di questa generazione tale operazione di norma è veloce. Gli oggetti sopravvissuti a questo processo risultano “invecchiati” e pertanto vengono trasferiti nella Old Generation. In genere per un oggetto che sta nella Young Generation viene impostata una soglia di età, superata la quale viene trasferito nella Vecchia Generazione.

Anche nella Old Generation avviene un recupero della memoria ed è quella che viene chiamata major garbage collection (major GC) . In generale gli eventi di garbage collection sono eventi di tipo “Stop the world” nel senso che quando avvengono, tutti i thread dell’applicazione vengono interrotti fino al completamento dell’operazione, ma una major GC spesso è molto più lenta di una minor GC in quanto coinvolge tutti gli oggetti ancora “vivi”; questa cosa è da tenere presente ad esempio, nelle applicazione in cui è richiesta una certa reattività, in cui bene limitare le major GC. Inoltre la durata di quest’ultimo tipo di eventi dipende dal tipo di Collector usato nella Old Generation; parleremo di questo nel prossimo paragrafo

Fino alla versione 7 di Java esisteva un’altra area dello heap chiamata Permanent Generation o (Perm Gen space) che conteneva metadati richiesti dalla JVM per descrivere le classi e i metodi usati nell’applicazione. Questa area di memoria veniva popolata dalla JVM a runtime con le classi utilizzate nell’applicazione e le classi e metodi del core di Java SE. È stato rimosso in Java 8. Quest’ultima generazione era coinvolta in quella che si chiama una Full GC in cui viene pulito l’intero heap.

 

Tipi di Garbage Collector

La JVM fornisce attualmente quattro diversi garbage collector, tutti generazionali. Ognuno ha i propri vantaggi e svantaggi. La scelta di quale garbage collector utilizzare dipende da noi e tale scelta può fare la differenza in termini di throughput e pause delle applicazioni.

Questi GC, suddividono l’heap in diversi segmenti, partendo dall’ipotesi  che la maggior parte degli oggetti nell’heap ha vita breve e dovrebbe essere riciclata rapidamente.

Ma vediamo meglio i quattro tipi di GC:

Serial GC

Questo è il garbage collector più semplice, progettato per sistemi a thread singolo e dimensioni di heap ridotte. Qui minor  e major GC vengono eseguiti sequenzialmente, viene inoltre usato un meccanismo di mark e compact che sposta la memoria più vecchia all’inizio dello heap in modo che le nuove allocazioni di memoria vengano trasformate in un singolo blocco continuo di memoria alla fine dello heap. Questa compattazione della memoria rende più veloce l’allocazione di nuovi blocchi di memoria nello heap.  Durante il lavoro congela tutte le applicazioni. In generale questo tipo di GC è adatto in quelle situazioni in cui un applicazione non abbia come requisito dei tempi di pausa brevi.  Può essere attivato utilizzando l’opzione della JVM -XX: + UseSerialGC.

Parallel / Throughput GC

Questo è il collector predefinito della JVM di Java 8. Come suggerisce il nome, utilizza più thread per analizzare lo spazio heap ed eseguire la compattazione. Uno svantaggio di questo collector è che mette in pausa i thread dell’applicazione mentre esegue un minor GC o un full GC. Per impostazione predefinita su un host con N CPU, il garbage collector parallelo utilizza N thread di raccolta automatica. Questo collector è adatto nei caso in cui si ha un carico di lavoro grosso ma sono accettabili pause lunghe. Ad esempio, un elaborazione in batch come stampare report o fatture o eseguire un numero elevato di query da un database. Il numero di thread del garbage collector può essere controllato con le opzioni della riga di comando:
-XX: ParallelGCThreads = <numero thread desiderato>

Concurrent Mark Sweep (CMS) Collector

Il collector Concurrent Mark Sweep (CMS) (chiamato anche il concurrent low pause collector). Questo meccanismo tenta di minimizzare le pause dovute alla garbage collection facendo in modo che la maggior parte delle garbage collection lavorino contemporaneamente ai thread dell’applicazione. Normalmente il collettore CMS non copia o compatta gli oggetti live cioè la garbage collection viene eseguita senza spostare gli oggetti in vita, questo di controparte può causare una frammentazione eccessiva della memoria che nel caso si verifichi può essere compensata allocando uno heap più grande. Il collector CMS deve essere utilizzato per applicazioni che richiedono tempi di pausa bassi e che possono condividere risorse con il GC. Alcuni esempi possono essere applicazioni di interfaccia utente desktop (GUI) che rispondono agli eventi, server web che rispondono a richieste o database che rispondono a query.

G1 Collector

Ultimo ma non meno importante è il collector Garbage-First, progettato per dimensioni di heap superiori ai 4 GB. Il G1 divide la dimensione dello heap in regioni che vanno da 1 a 32 Mb, in base alla dimensione dello heap. In questo tipo di collector esiste una fase di marcatura globale concorrente per determinare la vivacità degli oggetti in tutto lo heap. Al termine della fase di marcatura, G1 sa quali regioni sono per lo più vuote e può raccogliere prima gli oggetti irraggiungibili da queste regioni, che di solito producono una grande quantità di spazio libero. G1, quaindi,  raccoglie prima queste regioni (contenenti spazzatura) e quindi da questo il nome Garbage-First. G1 utilizza anche un modello di previsione delle pause in modo da soddisfare il tempo di pausa definito dall’utente in modo tale che il numero di regioni da “ripulire” permetta di rispettare tale parametro.

Garbage Collection Cycle

Ad alto livello, il collector G1 si alterna tra due fasi:

  1. La fase young-only che comincia con la raccolta di alcuni oggetti della young generation pronti per essere trasferiti nella Old generation.  La transizione tra questa fase  e quella di bonifica dello spazio inizia quando l’occupazione della vecchia generazione raggiunge una certa soglia. A questo punto, G1 pianifica una raccolta young-only con una marcatura Iniziale invece di una normale raccolta young-only.
  2. Fase di bonifica dello spazio: questa fase consiste in più raccolte miste: oltre alle regioni di nuova generazione, evacua anche oggetti vivi di regioni di vecchia generazione. La fase di bonifica dello spazio termina quando G1 determina che evacuare più regioni di vecchia generazione non produrrebbe abbastanza spazio libero da valerne la pena.

La figura sotto fornisce una panoramica su questo ciclo con un esempio della sequenza di pause e raccolta di dati inutili:

Java: La Garbage Collection 5

 

G1 può essere abilitato da riga di comando con: –XX:+UseG1GC

La strategia adottata nel G1 ha ridotto le possibilità di esaurimento dello heap prima che i thread in background abbiano completato la scansione di oggetti non raggiungibili.

In Java 8 viene fornita una bella ottimizzazione con il raccoglitore G1, chiamato string deduplication. Come sappiamo, gli array di caratteri che rappresentano le stringhe occupano molto spazio nello heap; è stata pertanto apportata una ottimizzazione che consente al collector G1 di identificare le stringhe che vengono duplicate più volte sul nostro heap e di modificarle in modo che facciano riferimento allo stesso array di caratteri interno. In questo modo si evita di avere nello heap copie multiple della stessa stringa .

Possiamo utilizzare a riga di comando l’argomento -XX: + UseStringDeduplication per abilitare questa ottimizzazione. G1 è il garbage collector predefinito nel JDK 9.

PermGen e Java 8 Metaspace

Come accennato in precedenza, lo spazio di generazione permanente (PermGen space) è stato rimosso da Java 8. La JVM  del JDK 8 utilizza invece la memoria nativa per la rappresentazione dei metadati di classe in una zona di memoria indicata come Metaspace.

C’è un nuovo flag MaxMetaspaceSize, per limitare la quantità di memoria utilizzata per i metadati della classe. Se non si specifica il valore per questo, il Metaspace viene opportunamente ridimensionato a runtime in base alle richieste dell’applicazione in esecuzione.

La garbage collection Metaspace viene attivata quando l’utilizzo dei metadati delle classe raggiunge il limite MaxMetaspaceSize. Un eccessiva raccolta dei dati di Metaspace può essere un sintomo di classes o classloaders memory leak o di un dimensionamento inadeguato del MaxMetaspaceSize per la nostra applicazione.

 

Riferimenti

Java Garbage Collection Basics

Garbage-First Garbage Collector

 

 

Lascia un commento

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.

Top