Java Virtual Machine - GC generazionali

La maggior parte delle JVM divide l'heap in tre generazioni: the young generation (YG), the old generation (OG) and permanent generation (also called tenured generation). Quali sono le ragioni dietro questo pensiero?

Studi empirici hanno dimostrato che la maggior parte degli oggetti creati ha una durata di vita molto breve -

fonte

https://www.oracle.com

Come puoi vedere, man mano che sempre più oggetti vengono allocati nel tempo, il numero di byte sopravvissuti diminuisce (in generale). Gli oggetti Java hanno un alto tasso di mortalità.

Analizzeremo un semplice esempio. La classe String in Java è immutabile. Ciò significa che ogni volta che è necessario modificare il contenuto di un oggetto String, è necessario creare un nuovo oggetto del tutto. Supponiamo di apportare modifiche alla stringa 1000 volte in un ciclo come mostrato nel codice seguente:

String str = “G11 GC”;

for(int i = 0 ; i < 1000; i++) {
   str = str + String.valueOf(i);
}

In ogni ciclo, creiamo un nuovo oggetto stringa e la stringa creata durante l'iterazione precedente diventa inutile (cioè non è referenziata da alcun riferimento). La durata di vita di quell'oggetto era solo un'iterazione: verranno raccolti dal GC in pochissimo tempo. Tali oggetti di breve durata sono conservati nell'area delle giovani generazioni del mucchio. Il processo di raccolta degli oggetti della giovane generazione è chiamato raccolta dei rifiuti minori e provoca sempre una pausa "ferma il mondo".

Man mano che la giovane generazione si riempie, il GC fa una piccola raccolta di rifiuti. Gli oggetti morti vengono scartati e gli oggetti attivi vengono spostati nella vecchia generazione. I thread dell'applicazione si interrompono durante questo processo.

Qui possiamo vedere i vantaggi che offre un design di tale generazione. La giovane generazione è solo una piccola parte del mucchio e si riempie rapidamente. Ma l'elaborazione richiede molto meno tempo rispetto al tempo impiegato per elaborare l'intero heap. Quindi, le pause "stop-theworld" in questo caso sono molto più brevi, anche se più frequenti. Dovremmo sempre mirare a pause più brevi rispetto a quelle più lunghe, anche se potrebbero essere più frequenti. Ne discuteremo in dettaglio nelle sezioni successive di questo tutorial.

La giovane generazione si divide in due spazi: eden and survivor space. Gli oggetti che sono sopravvissuti durante la raccolta dell'eden vengono spostati nello spazio dei sopravvissuti e quelli che sopravvivono allo spazio dei sopravvissuti vengono spostati nella vecchia generazione. La giovane generazione viene compattata mentre viene raccolta.

Quando gli oggetti vengono spostati nella vecchia generazione, alla fine si riempie e deve essere raccolto e compattato. Diversi algoritmi adottano approcci diversi a questo. Alcuni di loro interrompono i thread dell'applicazione (il che porta a una lunga pausa "stop-the-world" poiché la vecchia generazione è abbastanza grande rispetto alla generazione giovane), mentre alcuni di loro lo fanno contemporaneamente mentre i thread dell'applicazione continuano a funzionare. Questo processo è chiamato GC completo. Due di questi collezionisti lo sonoCMS and G1.

Analizziamo ora questi algoritmi in dettaglio.

GC seriale

è il GC predefinito sulle macchine di classe client (macchine a processore singolo o JVM 32b, Windows). In genere, i GC sono fortemente multithread, ma il GC seriale non lo è. Ha un singolo thread per elaborare l'heap e interromperà i thread dell'applicazione ogni volta che esegue un GC minore o un GC principale. Possiamo comandare alla JVM di utilizzare questo GC specificando il flag:-XX:+UseSerialGC. Se vogliamo che utilizzi un algoritmo diverso, specifica il nome dell'algoritmo. Si noti che la vecchia generazione è completamente compattata durante un GC principale.

Velocità effettiva GC

Questo GC è predefinito su JVM 64b e macchine multi-CPU. A differenza del GC seriale, utilizza più thread per elaborare i giovani e la vecchia generazione. Per questo motivo, il GC è anche chiamatoparallel collector. Possiamo comandare alla nostra JVM di utilizzare questo raccoglitore utilizzando il flag:-XX:+UseParallelOldGC o -XX:+UseParallelGC(per JDK 8 in poi). I thread dell'applicazione vengono interrotti mentre esegue una garbage collection principale o secondaria. Come il collezionista seriale, compatta completamente la giovane generazione durante un importante GC.

Il throughput GC raccoglie l'YG e l'OG. Quando l'eden si è riempito, il raccoglitore espelle gli oggetti vivi da esso nell'OG o in uno degli spazi superstiti (SS0 e SS1 nel diagramma sotto). Gli oggetti morti vengono scartati per liberare lo spazio che occupavano.

Prima di GC di YG

Dopo GC di YG

Durante un GC completo, il raccoglitore di velocità effettiva svuota l'intero YG, SS0 e SS1. Dopo l'operazione, l'OG contiene solo oggetti live. Dobbiamo notare che entrambi i collector di cui sopra arrestano i thread dell'applicazione durante l'elaborazione dell'heap. Ciò significa lunghe pause di "stop the world" durante un importante GC. I prossimi due algoritmi mirano ad eliminarli, a scapito di maggiori risorse hardware -

CMS Collector

Sta per "concurrent mark-sweep". La sua funzione è che utilizza alcuni thread in background per scansionare periodicamente la vecchia generazione e sbarazzarsi di oggetti morti. Ma durante un GC minore, i thread dell'applicazione vengono interrotti. Tuttavia, le pause sono piuttosto piccole. Questo rende il CMS un raccoglitore a bassa pausa.

Questo collector richiede tempo CPU aggiuntivo per eseguire la scansione dell'heap durante l'esecuzione dei thread dell'applicazione. Inoltre, i thread in background raccolgono solo l'heap e non eseguono alcuna compattazione. Possono portare alla frammentazione dell'heap. Dato che ciò continua, dopo un certo punto di tempo, il CMS interromperà tutti i thread dell'applicazione e compatterà l'heap utilizzando un singolo thread. Utilizzare i seguenti argomenti JVM per dire alla JVM di utilizzare il raccoglitore CMS:

“XX:+UseConcMarkSweepGC -XX:+UseParNewGC” come argomenti JVM per dirgli di utilizzare il raccoglitore CMS.

Prima di GC

Dopo GC

Notare che la raccolta viene eseguita contemporaneamente.

G1 GC

Questo algoritmo funziona dividendo l'heap in un numero di regioni. Come il raccoglitore CMS, arresta i thread dell'applicazione mentre esegue un GC minore e utilizza thread in background per elaborare la vecchia generazione mantenendo i thread dell'applicazione in esecuzione. Poiché ha diviso la vecchia generazione in regioni, continua a compattarle mentre sposta gli oggetti da una regione all'altra. Quindi, la frammentazione è minima. Puoi usare la bandiera:XX:+UseG1GCper dire alla tua JVM di utilizzare questo algoritmo. Come CMS, richiede anche più tempo della CPU per l'elaborazione dell'heap e l'esecuzione simultanea dei thread dell'applicazione.

Questo algoritmo è stato progettato per elaborare heap più grandi (> 4G), che sono suddivisi in un numero di regioni diverse. Alcune di queste regioni comprendono le giovani generazioni e il resto comprendono le vecchie. L'YG viene cancellato usando tradizionalmente: tutti i thread dell'applicazione vengono arrestati e tutti gli oggetti che sono ancora vivi nella vecchia generazione o nello spazio dei sopravvissuti.

Si noti che tutti gli algoritmi GC hanno diviso l'heap in YG e OG e utilizzano un STWP per cancellare l'YG. Questo processo è generalmente molto veloce.