W3docs

Algoritmi GC in Java

Confronta i principali garbage collector Java — Serial, Parallel, G1, ZGC e Shenandoah — e i loro compromessi.

La JVM ti libera dalla deallocazione manuale della memoria: un garbage collector (GC) gira in background, individua gli oggetti che il programma non può più raggiungere e ne recupera lo spazio. Ma "il garbage collector" non è una cosa sola. La JVM HotSpot include diversi collector, ognuno dei quali stabilisce un compromesso diverso tra throughput (quanta CPU va alla tua applicazione invece che al GC), latenza (per quanto tempo l'applicazione si ferma mentre il GC lavora) e footprint (quanto overhead di memoria richiede il collector stesso).

Scegliere bene è importante. Un job batch che elabora dati tutta la notte vuole il massimo throughput e non si preoccupa delle pause; un sistema di trading o un web server vuole le pause più brevi possibili anche se il throughput totale cala un po'. Questo capitolo confronta i collector in produzione e mostra cosa hanno tutti in comune: un oggetto vive esattamente finché è raggiungibile.

Come i collector decidono cosa mantenere

Ogni collector HotSpot risponde alla stessa domanda — quali oggetti sono ancora in uso — nello stesso modo: traccia la raggiungibilità a partire da un insieme di GC root (variabili locali nello stack, campi statici, thread attivi). Qualsiasi oggetto raggiungibile da una root è vivo; tutto il resto è spazzatura. Lo scope e l'età sono irrilevanti; conta solo la raggiungibilità. Per il quadro più ampio di come questo si inserisce nel runtime, vedi Java Garbage Collection e Stack vs Heap.

public class Reachability {
    public static void main(String[] args) {
        String a = new String("kept");      // reachable via local variable 'a'
        String b = new String("dropped");   // reachable via 'b'...
        b = null;                            // ...until now: "dropped" is unreachable
        System.out.println(a);               // 'a' is still a GC root reference
    }
}

Nel momento in cui viene eseguito b = null, l'oggetto "dropped" non ha alcun percorso da nessuna root e diventa eleggibile per la raccolta. Il collector può recuperarlo immediatamente, molto più tardi, o — se il programma termina prima — mai. Non chiami mai free; smetti semplicemente di fare riferimento all'oggetto.

Layout generazionale dell'heap

La maggior parte degli oggetti Java muore giovane. I collector sfruttano questo con un heap generazionale: i nuovi oggetti atterrano nella young generation (Eden più due spazi survivor), e gli oggetti che sopravvivono a diverse raccolte vengono promossi alla old generation. Raccogliere frequentemente la piccola young generation, ricca di spazzatura, è economico; la grande old generation viene raccolta molto meno spesso.

RegioneCosa ci viveCon quale frequenza viene raccolta
EdenOggetti appena allocatiAd ogni minor GC
Survivor (S0/S1)Oggetti sopravvissuti a un minor GCAd ogni minor GC
Old (tenured)Oggetti longevi, promossiMajor / full GC
MetaspaceMetadati delle classi (off-heap)Al caricamento delle classi

Un minor GC pulisce la young generation ed è veloce; un major o full GC tocca la old generation ed è la fonte delle pause lunghe che tutti temono.

Confronto tra i collector

HotSpot ti permette di scegliere un collector con un singolo flag, e ognuno è ottimizzato per un obiettivo diverso. Raramente si cambia l'algoritmo nel codice — lo si imposta dalla riga di comando.

java -XX:+UseSerialGC     MyApp   # single-threaded, tiny heaps
java -XX:+UseParallelGC   MyApp   # throughput-oriented, multi-threaded
java -XX:+UseG1GC         MyApp   # balanced, the default since Java 9
java -XX:+UseZGC          MyApp   # sub-millisecond pauses, huge heaps
java -XX:+UseShenandoahGC MyApp   # low pause, concurrent compaction

La tabella seguente è il modello mentale da tenere a mente:

CollectorPunto di forzaComportamento delle pauseUtilizzo tipico
SerialIl più semplice, footprint ridottoStop-the-world, singolo threadHeap piccoli, container, CLI
ParallelThroughput più elevatoStop-the-world, molti threadBatch / elaborazione dati
G1Bilanciato, prevedibilePrevalentemente concorrente, target di pausaDefault per uso generale
ZGCLatenza molto bassaSub-millisecondo, concorrenteHeap da multi-GB a TB
ShenandoahLatenza molto bassaPause indipendenti dalla dimensione dell'heapServizi responsive

G1 ("Garbage-First") è il default da Java 9 in poi. Divide l'heap in regioni di uguale dimensione e raccoglie prima le regioni con più spazzatura, puntando a un obiettivo di tempo di pausa impostato con -XX:MaxGCPauseMillis=200.

Concorrente vs stop-the-world

L'asse cruciale è quando i thread dell'applicazione devono fermarsi. I collector stop-the-world (STW) (Serial, Parallel) mettono in pausa tutti i thread dell'applicazione mentre lavorano — semplice e ad alto throughput, ma la pausa cresce con l'heap. I collector concorrenti (ZGC, Shenandoah e la maggior parte di G1) svolgono la maggior parte del loro lavoro mentre i tuoi thread continuano a girare, quindi le pause rimangono brevi anche quando gli heap raggiungono gigabyte o terabyte.

# See exactly what the collector is doing and how long it pauses
java -Xlog:gc -XX:+UseG1GC MyApp
# Sample output line:
# [0.412s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 24M->5M(64M) 1.832ms

Vale la pena decodificare quella riga di log: 24M->5M(64M) significa che l'heap utilizzato è sceso da 24 MB a 5 MB su un totale di 64 MB, e l'applicazione si è fermata per 1.832ms. Leggere l'output di -Xlog:gc è la singola abilità di tuning del GC più utile — misura prima di cambiare qualsiasi flag.

Osservare la raccolta dal codice

Non puoi invocare direttamente un algoritmo specifico da Java, ma puoi osservare la raccolta in corso. Una WeakReference ti permette di mantenere un puntatore che non tiene in vita il suo target, così puoi chiedere "questo oggetto è già stato raccolto?" La classe Runtime riporta l'utilizzo dell'heap, e System.gc() è un suggerimento — mai un comando — per eseguire una raccolta ora.

import java.lang.ref.WeakReference;

WeakReference<byte[]> ref = new WeakReference<>(new byte[1024]);
System.out.println("Before GC: " + (ref.get() != null)); // true
System.gc();
System.out.println("After GC:  " + (ref.get() != null)); // usually false

L'esempio eseguibile qui sotto mette insieme questi elementi: alloca un'ondata di spazzatura, mantiene un survivor raggiungibile, osserva un oggetto irraggiungibile attraverso una weak reference e misura l'heap prima e dopo una raccolta.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Il passo 2 stampa true: l'oggetto osservato è ancora fortemente raggiungibile attraverso la variabile garbage, quindi la WeakReference può leggerlo.
  • Il passo 3 riporta l'heap utilizzato dopo 300.000 allocazioni di breve durata. Il numero esatto varia da un'esecuzione all'altra — un minor GC potrebbe aver già spazzato gran parte di quell'ondata a metà loop — ma crearla è esattamente il tipo di attività nella young generation che ogni collector generazionale è costruito per gestire in modo economico.
  • Il passo 4 stampa true, confermando che l'oggetto osservato è stato recuperato una volta che garbage = null lo ha reso irraggiungibile e System.gc() ha avviato una raccolta — prova che la perdita di raggiungibilità, non l'uscita dallo scope, è ciò che libera la memoria.
  • Il passo 5 stampa true: il survivor, ancora referenziato da una variabile locale attiva (una GC root), attraversa la raccolta intatto.
  • Il passo 6 mostra l'heap utilizzato che torna vicino al valore di riferimento, dimostrando che il collector restituisce lo spazio recuperato per il riutilizzo invece di essere perso dal programma.

Per ulteriori informazioni sui tipi di reference usati qui — strong, soft, weak e phantom — vedi Java References.

Esercitazione

Pratica
Quale singola proprietà determina se il garbage collector manterrà un oggetto, indipendentemente dall'algoritmo utilizzato?
Quale singola proprietà determina se il garbage collector manterrà un oggetto, indipendentemente dall'algoritmo utilizzato?
Was this page helpful?