W3docs

Garbage Collection in Java

Come funziona il garbage collection in Java: raggiungibilità, heap generazionale, mark-sweep-compact, tipi di riferimento e collector disponibili.

In Java non si chiama mai free(). La JVM traccia ogni oggetto allocato sull'heap e, quando un oggetto non è più raggiungibile dal programma in esecuzione, il garbage collector (GC) ne recupera la memoria automaticamente. Tu scrivi codice che crea oggetti; il GC pulisce silenziosamente dietro di te. Capire come decide cosa è spazzatura — e dove nell'heap cerca — è la differenza tra codice che scala e codice che si blocca sotto carico.

Questa pagina illustra come il GC decide cosa conservare (raggiungibilità), come è strutturato l'heap per una raccolta efficiente, l'algoritmo mark-sweep-compact, i quattro tipi di riferimento, come scegliere un collector e un esempio eseguibile che rende la raccolta osservabile.

Raggiungibilità e GC root

Il GC non cerca gli oggetti di cui hai "finito". Cerca gli oggetti ancora raggiungibili. Partendo da un insieme di GC root, segue ogni riferimento. Tutto ciò che riesce a raggiungere è vivo; tutto il resto è spazzatura, indipendentemente dal fatto che tu pensi di averne ancora bisogno.

GC rootEsempio
Variabili localiUn riferimento nello stack di un thread in esecuzione
Campi staticistatic final Logger LOG = ...
Thread attiviUn oggetto Thread vivo
Riferimenti JNIOggetti detenuti da codice nativo

Impostare un riferimento a null (o lasciarlo uscire dallo scope) non elimina nulla — rimuove solo un percorso verso l'oggetto. L'oggetto diventa raccoglibile solo quando non rimane nessun percorso da nessun root.

Object a = new Object();   // reachable via local variable 'a'
Object b = a;              // now two references point to the same object
a = null;                  // still reachable through 'b' — not garbage
b = null;                  // now unreachable — eligible for collection

L'heap generazionale

La maggior parte degli oggetti muore giovane — uno scope di richiesta, un temporaneo di ciclo, una stringa intermedia. La JVM sfrutta questa ipotesi generazionale debole dividendo l'heap in regioni e raccogliendo l'area giovane molto più spesso di quella vecchia.

RegioneContieneRaccolta
Young (Eden + 2 spazi Survivor)Oggetti appena allocatiFrequentemente, tramite una minor GC veloce
Old (Tenured)Oggetti sopravvissuti a diverse minor GCRaramente, tramite una major/full GC più lenta
MetaspaceMetadati delle classi (non i tuoi oggetti)Quando i classloader vengono scaricati

I nuovi oggetti finiscono in Eden. Una minor GC copia i pochi sopravvissuti in uno spazio Survivor; gli oggetti che continuano a sopravvivere vengono eventualmente promossi alla generazione vecchia. Poiché le minor GC scansionano solo la piccola regione giovane, sono economiche — ecco perché l'allocazione di oggetti a breve durata in Java è veloce. (Per come differiscono stack e heap, vedi Stack vs Heap; per il ruolo della JVM che ospita l'heap, vedi Architettura JVM.)

Mark, sweep, compact

Una raccolta si esegue in fasi. Prima marca ogni oggetto raggiungibile percorrendo il grafo dai root. Poi spazza, liberando gli oggetti non marcati. Molti collector aggiungono una fase di compattazione che sposta gli oggetti sopravvissuti uno accanto all'altro così che lo spazio libero sia un blocco contiguo — il che mantiene l'allocazione un semplice incremento di puntatore e previene la frammentazione.

// Pseudocode of what the collector does for you:
// 1. mark:    visit(roots); for each reachable object, set live = true
// 2. sweep:   for each object on the heap, if !live -> reclaim its memory
// 3. compact: move survivors next to each other, update references

Puoi suggerire una raccolta con System.gc(), ma è solo un suggerimento — la JVM potrebbe ignorarlo. Non fare mai affidamento su di esso per la correttezza; trattalo come uno strumento diagnostico, non una strategia di gestione della memoria.

Forza dei riferimenti

Non ogni riferimento mantiene un oggetto vivo allo stesso modo. Il package java.lang.ref ti permette di indicare al GC quanto tieni a conservare un oggetto, che è la base delle cache sensibili alla memoria.

RiferimentoComportamento del GC
Forte (ordinario =)Mai raccolta mentre raggiungibile
SoftReferenceRaccolta solo quando la memoria è scarsa — buona per le cache
WeakReferenceRaccolta al prossimo GC una volta che non rimangono riferimenti forti
PhantomReferenceUsata per pianificare la pulizia dopo la raccolta
import java.lang.ref.WeakReference;

byte[] data = new byte[1024];
WeakReference<byte[]> ref = new WeakReference<>(data);
data = null;            // drop the only strong reference
// After the next GC, ref.get() may return null.

I memory leak avvengono lo stesso

Un garbage collector ti libera dai dangling pointer e dai double free, ma non dai leak. Un memory leak Java è un oggetto che non usi più ma che è ancora raggiungibile da un root, quindi il GC deve conservarlo. L'heap si riempie, il GC gira sempre più spesso, e alla fine si raggiunge un OutOfMemoryError.

Le cause classiche sono tutte "ho dimenticato di mollare":

  • Una collection static (cache, lista di listener, mappa) a cui continui ad aggiungere elementi senza mai rimuoverli. I campi statici sono GC root, quindi tutto ciò che raggiungono vive per sempre.
  • Listener o callback registrati su un oggetto longevo e mai deregistrati.
  • Chiavi rimaste in una HashMap molto dopo che non sono più necessarie, perché la mappa le referenzia ancora.

La soluzione non è un flag — è rilasciare i riferimenti quando hai finito (rimuovi dalla collection, deregistra il listener) oppure usare una struttura basata su WeakReference come WeakHashMap affinché il GC possa recuperare le voci non appena nulla punta più alla chiave.

Attenzione
Java non ha distruttori, e finalize() è deprecato e inaffidabile — la JVM potrebbe eseguirlo tardi o non eseguirlo affatto. Per rilasciare file, socket o altre risorse non di memoria in modo deterministico, usa try-with-resources e AutoCloseable, non il garbage collector.

Scegliere un collector

La JVM HotSpot include diversi collector con diversi compromessi tra throughput (lavoro totale svolto) e latenza (durata delle pause). Si sceglie con un flag JVM; il predefinito dal Java 9 è G1.

CollectorFlagOttimo per
G1 (predefinito)-XX:+UseG1GCLatenza/throughput bilanciati, heap grandi
Parallel-XX:+UseParallelGCJob batch che privilegiano il throughput grezzo
ZGC-XX:+UseZGCHeap molto grandi, pause sotto il millisecondo
Serial-XX:+UseSerialGCHeap piccoli, singolo core o container
# Pick a collector and set the heap size at launch:
java -XX:+UseG1GC -Xms256m -Xmx2g MyApp

# Print what the GC is doing, with timestamps:
java -Xlog:gc* MyApp

Un esempio pratico

Il programma seguente rende osservabile il comportamento del GC. Mantiene un oggetto con un riferimento forte, ne tiene un altro solo tramite una WeakReference, genera una serie di spazzatura a breve durata, poi richiede una raccolta e riporta cosa è sopravvissuto e come è cambiato l'utilizzo dell'heap.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • Il referente debole stampa true prima della raccolta e false dopo, dimostrando che una WeakReference non mantiene vivo il suo oggetto una volta che non rimangono riferimenti forti.
  • L'array kept con riferimento forte stampa survived: true anche dopo System.gc(), perché è raggiungibile da un GC root e il collector deve preservarlo.
  • Circa 300 MB di spazzatura viene allocata (Bytes allocated as garbage: 307200000), eppure l'heap usato sale solo a circa 5 MB — le minor GC recuperano gli array del ciclo a breve durata man mano che vengono creati.
  • Runtime.maxMemory() riporta il limite dell'heap (circa 256 MB qui), impostato da -Xmx, mentre totalMemory() - freeMemory() è la porzione viva usata che rimane intorno a 3–5 MB per tutta la durata.
  • System.gc() è solo un suggerimento, ma su questa JVM viene eseguito: l'heap usato scende di nuovo e il referente debole non raggiungibile viene azzerato invece di restare in memoria.

Pratica

Pratica
Un oggetto è referenziato solo da una variabile locale appena uscita dallo scope, e da nient'altro. Cosa lo rende eleggibile per il garbage collection?
Un oggetto è referenziato solo da una variabile locale appena uscita dallo scope, e da nient'altro. Cosa lo rende eleggibile per il garbage collection?
Was this page helpful?