1. Introduzione al garbage collector (GC)
Se hai mai programmato in C o C++, ti sarai sicuramente imbattuto nella necessità di liberare manualmente la memoria con free() o delete. In Java è molto più semplice: crei un oggetto con new e non devi eliminarlo — se ne occupa uno «spazzino» speciale chiamato garbage collector (Garbage Collector, GC).
Il GC è una parte della JVM che libera automaticamente la memoria occupata dagli oggetti a cui non esistono più riferimenti. Grazie a esso gli sviluppatori Java non devono preoccuparsi di dimenticare di liberare la memoria (con conseguenti leak) o, al contrario, di eliminare per errore un oggetto ancora necessario (e causare un crash).
Ma, come ogni spazzino, il GC non è perfetto: a volte può intervenire nel momento meno opportuno, avviando una «pulizia generale» (Stop-the-World), oppure non essere veloce quanto vorremmo. Per questo nella JVM esistono diverse implementazioni del garbage collector — e la scelta corretta può influire sensibilmente sulle prestazioni dell’applicazione.
Tipi principali di garbage collector
Serial GC
- Serial GC — il garbage collector più semplice e antico.
- Lavora su un solo thread.
- Ferma tutti gli altri thread durante la raccolta (Stop-the-World).
- Adatto a piccole applicazioni senza multithreading intenso.
- Si abilita con il flag: -XX:+UseSerialGC
Parallel GC
- Parallel GC (anche «Throughput Collector»).
- Usa più thread per la raccolta dei rifiuti.
- Orientato alla massima throughput.
- Esegue comunque la raccolta con pause Stop-the-World, ma più velocemente di Serial.
- Adatto ad applicazioni server in cui piccole pause non sono critiche.
- Si abilita con il flag: -XX:+UseParallelGC
CMS (Concurrent Mark Sweep)
- CMS — GC deprecato ma a lungo popolare, che minimizza le pause.
- Lavora in parte in parallelo con l’applicazione, riducendo il tempo di stop.
- Più complesso da configurare, con overhead aggiuntivo.
- Dalla Java 9 è contrassegnato come deprecato (deprecated).
- Si abilita con il flag: -XX:+UseConcMarkSweepGC
G1 (Garbage First)
- G1 GC — garbage collector moderno predefinito (a partire da Java 9).
- Bilancia pause minime e prestazioni.
- Divide l’heap in molti piccoli regioni (modello a regioni).
- Può raccogliere selettivamente per regioni, senza toccare l’intero heap.
- Permette di impostare una pausa massima target, ad esempio -XX:MaxGCPauseMillis=200.
- Flag di abilitazione: -XX:+UseG1GC (di solito non serve, perché G1 è predefinito).
ZGC e Shenandoah
- ZGC e Shenandoah — garbage collector moderni a bassa latenza.
- Obiettivo: pause minime (millisecondi), anche su heap enormi (fino a terabyte).
- Lavorano praticamente in pieno parallelo con l’applicazione.
- Richiedono Java 11+ (ZGC) o Java 12+ (Shenandoah).
- Adatti a sistemi sensibili alla latenza (borse, fintech, analitica in tempo reale).
- Flag di abilitazione: -XX:+UseZGC oppure -XX:+UseShenandoahGC
3. Principi di funzionamento dei GC moderni
Generazione giovane e vecchia (Young/Old Generation)
La JVM divide l’heap in due grandi parti:
Generazione giovane (Young Generation): qui finiscono tutti i nuovi oggetti. Qui la raccolta avviene spesso e rapidamente ( Minor GC ).
Generazione vecchia (Old Generation, Tenured): qui migrano gli oggetti che sono «sopravvissuti» a diverse raccolte nella generazione giovane. Qui la raccolta avviene più raramente ma dura più a lungo ( Major/Full GC ).
Perché così? La maggior parte degli oggetti in Java vive molto poco (ad esempio, stringhe temporanee, collezioni all’interno di un metodo). Perciò si può pulire la generazione giovane rapidamente e spesso, senza toccare la vecchia.
Minor GC
- Pulisce solo la generazione giovane.
- Veloce, con una pausa breve.
- Non tocca gli oggetti vecchi.
Major (Full) GC
- Pulisce l’intero heap (sia generazione giovane che vecchia).
- Può richiedere molto tempo (secondi e oltre su heap grandi).
- Di solito è accompagnato da una lunga pausa dell’applicazione.
Come il GC determina quali oggetti eliminare?
Il GC cerca gli oggetti «vivi» partendo dai riferimenti radice (root set): variabili locali negli stack dei thread, campi statici, parametri dei metodi, ecc. Tutto ciò a cui si può «arrivare» è considerato vivo. Il resto è spazzatura.
4. Confronto dei garbage collector moderni: G1, ZGC, Shenandoah
Vediamo in cosa differiscono i GC più moderni e diffusi. A tal fine, ecco una tabella riassuntiva:
| Collector | Obiettivo principale | Modello di memoria | Pause minime | Scalabilità | Supporto | Quando usarlo |
|---|---|---|---|---|---|---|
| G1 | Equilibrio tra pause/velocità | Regioni | ~10–200 ms | Fino a centinaia di GB | Java 9+ (predefinito) | La maggior parte delle applicazioni server |
| ZGC | Pausa minima | Regioni, «tag colorati» | <10 ms | Fino a terabyte | Java 11+ | Tempo reale, latency‑critical |
| Shenandoah | Pausa minima | Regioni, «tag colorati» | <10 ms | Fino a terabyte | Java 12+ (RedHat) | Tempo reale, latency‑critical |
G1 GC: Garbage First
- Divide l’heap in molte regioni (di solito 1–32 MB ciascuna).
- Durante la raccolta sceglie le regioni con più spazzatura («garbage first»).
- Può raccogliere solo una parte dell’heap, non tutto in una volta.
- Permette di impostare la pausa target: -XX:MaxGCPauseMillis=200.
- Adatto a bilanciare velocità e pause; usato di default da Java 9.
Esempio di abilitazione (se fosse disabilitato):
java -XX:+UseG1GC -jar myapp.jar
ZGC: Z Garbage Collector
- Sperimentale in Java 11, stabile da Java 15.
- Quasi non ferma l’applicazione: le pause sono di solito <10 ms, anche con 1–2 TB di heap.
- Usa «tag colorati» (coloring) e puntatori speciali.
- Richiede una JVM a 64 bit; non funziona su sistemi a 32 bit.
- Supportato su Linux, macOS, Windows.
Esempio di abilitazione:
java -XX:+UseZGC -jar myapp.jar
Shenandoah
- Sviluppato da RedHat; obiettivi simili a ZGC.
- Pause minime, lavoro parallelo attivo con l’applicazione.
- Supporto per Linux e Windows; parte delle build OpenJDK.
- Usa tecniche simili, ma algoritmi interni diversi.
Esempio di abilitazione:
java -XX:+UseShenandoahGC -jar myapp.jar
Confronto visivo
graph TD
A[Generazione giovane] -->|Minor GC| B[Generazione vecchia]
B -->|Major GC| C[GC Pause]
D[G1: regioni] --> E[Regioni selettive]
F[ZGC/Shenandoah: regioni] --> G[Raccolta parallela]
5. Pratica: come scoprire e modificare il GC
Come sapere quale GC è in uso?
- Log della JVM: Avvia l’applicazione con i parametri -Xlog:gc* (Java 9+) o -verbose:gc (fino a Java 8). Nei log vedrai quale GC è usato e quanto spesso si verificano le pause.
- jcmd: Esegui:
dove <pid> è l’identificatore del processo Java.jcmd <pid> VM.flags - jvisualvm: Nella sezione «Monitoraggio» puoi vedere il tipo di GC.
Come cambiare il GC per la tua applicazione?
Aggiungi il flag desiderato all’avvio del programma Java:
G1 GC (predefinito, puoi specificarlo esplicitamente):
java -XX:+UseG1GC -jar myapp.jar
ZGC:
java -XX:+UseZGC -jar myapp.jar
Shenandoah:
java -XX:+UseShenandoahGC -jar myapp.jar
Come impostare la dimensione dell’heap e le pause?
- Dimensione massima dell’heap: -Xmx2G
- Dimensione minima dell’heap: -Xms512M
- Per G1: pausa desiderata — -XX:MaxGCPauseMillis=200
Esempio di avvio completo:
java -Xms512M -Xmx2G -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar myapp.jar
6. Linee guida per la scelta del GC in vari scenari
Quando scegliere G1
- Nella maggior parte delle applicazioni server e desktop — un’ottima scelta predefinita.
- Funziona bene su heap da centinaia di megabyte a centinaia di gigabyte.
- Bilancia prestazioni e pause.
Quando scegliere ZGC o Shenandoah
- Se l’applicazione è sensibile alla latenza (latency‑critical: borse, giochi online, analitica in tempo reale).
- Se l’heap è enorme (centinaia di gigabyte o più).
- Se sono ammesse solo pause minime (millisecondi).
- Richiesta Java 11+ (ZGC) o Java 12+ (Shenandoah).
Quando basta Parallel GC
- Per piccole applicazioni in cui conta la massima throughput e le pause non sono critiche.
- Per l’elaborazione batch, dove si può «sopportare» uno stop per il Full GC.
7. Esempio: confronto del comportamento dei GC su una semplice applicazione
Una piccola applicazione che genera molti oggetti temporanei (simulazione dell’elaborazione di ordini):
public class GCSimulator {
public static void main(String[] args) {
while (true) {
// Creiamo 100.000 oggetti a ogni ciclo
for (int i = 0; i < 100_000; i++) {
String s = new String("Order-" + i);
}
// Ci prendiamo una breve pausa
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}
}
Avvialo con GC diversi e guarda i log:
java -Xmx256M -XX:+UseG1GC -Xlog:gc* GCSimulator
java -Xmx256M -XX:+UseZGC -Xlog:gc* GCSimulator
Cosa vedrai?
G1 effettuerà pause frequenti ma brevi. ZGC/Shenandoah — pause ancora più brevi, ma potenzialmente più frequenti. Parallel GC — pause più lunghe, ma più rare.
8. Errori tipici e sfumature nell’uso del GC
Errore n. 1: aspettarsi che il GC risolva tutti i problemi di memoria. Il GC non è una bacchetta magica. Se mantieni riferimenti a oggetti inutili, nessun GC potrà aiutare — avrai una perdita di memoria (memory leak).
Errore n. 2: invocare forzatamente System.gc(). La JVM sa meglio quando raccogliere. Un GC forzato può causare una lunga pausa e ridurre le prestazioni.
Errore n. 3: ignorare i log del GC. Se non osservi i log del GC, potresti non accorgerti che la tua applicazione «si blocca» regolarmente per il Full GC.
Errore n. 4: usare GC deprecati. Ad esempio, CMS non è più sviluppato. È meglio passare a G1 o a collector moderni a bassa latenza.
Errore n. 5: scegliere il GC sbagliato per il caso d’uso. Se hai un’applicazione latency‑critical e usi Parallel GC — aspettati pause lunghe. Se fai elaborazione batch e abiliti ZGC — incorrerai in overhead non necessari.
GO TO FULL VERSION