I Vari Tipi Di Garbage Collector In Java

La JVM ha diversi tipi di implementazioni del GC:

  • Serial
  • Parallel
  • G1
  • ZGC
  • Shenandoah

Ognuno di essi ha meccanismi interni e opzioni di tuning propri. Diamo loro una breve introduzione.

Serial GC

Il Serial GC è uno dei primi GC della JVM. Utilizza un singolo thread per eseguire tutto il lavoro di pulizia. La sua caratteristica fondamentale è che, durante l’esecuzione del processo di Garbage Collection, la JVM deve mettere in pausa tutti i thread dell’applicazione. Questo evento è noto come “Stop-The-World” (STW). Poiché c’è un solo thread dedicato al GC, le pause STW possono essere relativamente lunghe se l’heap (l’area di memoria) è di grandi dimensioni. D’altra parte, la sua natura a thread singolo lo rende incredibilmente leggero in termini di overhead computazionale. Non dovendo gestire la sincronizzazione o la comunicazione tra thread multipli, il Serial GC consuma pochissime risorse della CPU (e RAM) per le sue operazioni interne. Le criticità principali sono:

  • Pause STW Lunghe (Latenza elevata): Se l’Heap supera il Gigabyte, il tempo necessario per marcare e compattare la memoria su un singolo thread diventa percepibile all’utente (si parla di decine o centinaia di millisecondi, a volte secondi).
  • Spreco di Risorse Hardware: Su server moderni con 16, 32 o 64 core, il Serial GC ne utilizzerà solo uno per la pulizia, lasciando gli altri inattivi mentre l’intera applicazione è bloccata.

Con l’avvento del G1GC (il default da Java 9) e dei low-latency GC (ZGC e Shenandoah), si potrebbe pensare che il Serial GC sia morto. In realtà, ha trovato una “seconda giovinezza” nel cloud computing e nei microservizi. E’ consigliabile abilitare il Serial GC su:

  1. Microservizi Dockerizzati molto piccoli: Se il container ha limiti stringenti (es. 1 o 2 vCPU e meno di 1–2 GB di RAM), G1GC sprecherebbe troppa memoria e cicli di CPU per la sua gestione interna. Il Serial GC qui brilla per leggerezza.
  2. Applicazioni Client: Applicazioni desktop Java minori in cui una pausa occasionale di qualche decimo di secondo non compromette l’esperienza utente.
  3. Applicazioni Batch: Script o job eseguiti in background dove la latenza non è importante, ma conta completare il lavoro con il minimo spreco di risorse.

 

 

Parallel GC

Dopo il Serial GC è stato introdotto il Parallel Garbage Collector. Esso utilizza più thread per eseguire la garbage collection, al contrario del Serial che usa un unico thread. Per il resto, entrambi i collector sono di tipo Stop-The-World (STW), il che significa che l’applicazione si ferma completamente durante la pulizia. Un’altra differenza fondamentale con il Serial GC è che usa un solo core CPU lavora mentre nel Parallel GC più core CPU lavorano insieme. Tutto ciò si traduce in pause più brevi per il Parallel.

Concurrent GC

Con il tempo sono stati introdotti nuovi tipi di GC: i Concurrent GC (G1, ZGC, Shenandoah). Essi si contraddistinguono dal funzionamento diverso rispetto ai STW GC (Serial / Parallel). In particolare, un collector concorrente esegue la maggior parte del lavoro di scansione e pulizia mentre l’applicazione sta ancora girando, invece di fermarla del tutto.

Quando Passare Al Concurrent?

Concurrent GC (G1 o ZGC) se:

  • Le pause superiori a 200ms causano timeout o lamentele degli utenti.
  • Heap molto grandi (sopra gli 8GB-16GB), dove una pausa “Parallel” sarebbe catastrofica.

Resta sul Parallel GC se:

  • L’obiettivo è finire un calcolo nel minor tempo possibile e non interessa se l’app non risponde per 2 secondi ogni ora.
  • Risorse CPU limitate e non si vuole che il GC “interferisca” con l’esecuzione dell’app.

Passiamo ora all’analisi del G1, al momento il default per le JVM moderne.

G1 GC

Il G1 (Garbage First) Collector è diventato il default da Java 9. A differenza del Parallel GC, che divide lo heap in tre aree contigue giganti (Eden, Survivor, Old), il G1 frammenta l’intero Heap in centinaia o migliaia di regioni di dimensioni identiche (da 1MB a 32MB ciascuna).

  • Ogni regione può essere dinamicamente assegnata alla Young Generation o alla Old Generation.
  • Esistono regioni speciali chiamate Humongous, progettate per ospitare oggetti che superano il 50% della dimensione di una regione standard, evitando che finiscano direttamente nella Old Gen creando frammentazione immediata.

A differenza del vecchio Parallel Collector che puliva l’intera Old Generation ogni volta (causando lunghe pause), G1 è incrementale. Si concentra sulle regioni che offrono il massimo rendimento (più spazio recuperato con il minimo sforzo), riuscendo a mantenere le pause prevedibili e brevi.

Il Ciclo di Vita del G1

Il G1 alterna due fasi principali, operando come un collector “incrementale”:

  • Young GC (Stop-The-World): Quando le regioni allocate come Eden si riempiono, G1 esegue una pausa STW. Essendo l’heap diviso in regioni, G1 può decidere di pulire solo un sottoinsieme di esse per rientrare in un target di pausa predefinito. Gli oggetti vivi vengono copiati nelle regioni Survivor o promossi nella Old Gen.
  • Mixed GC: Questa è la fase che lo distingue. Invece di pulire l’intera Old Gen in una volta sola (che causerebbe una pausa enorme come nel Parallel), lo fa in diversi cicli più piccoli e veloci. G1 inizia a pulire sia le regioni della Young Gen sia alcune regioni della Old Gen. Anche in questo caso c’è una STW ma molto più breve rispetto al Parallel

In realtà c’è anche una terza fase che è meglio evitare: Full GC.

Se l’applicazione produce oggetti più velocemente di quanto G1 riesca a pulire, o se non c’è abbastanza spazio per muovere gli oggetti (Heap Fragmentation), scatta una Full GC. È un evento pesante, a thread singolo o parallelo ma molto lento, che compatta l’intero Heap. L’obiettivo della configurazione di G1 è proprio quello di evitare che si arrivi a questo punto.

 

ZGC

ZGC (Z Garbage Collector) rappresenta una delle innovazioni più significative introdotte recentemente nell’ecosistema Java (disponibile come “production-ready” da Java 15). È un garbage collector scalabile e a bassissima latenza, progettato per gestire heap che vanno da pochi megabyte a svariati terabyte, mantenendo i tempi di pausa inferiori al millisecondo.

L’obiettivo primario di ZGC non è il throughput (ovvero la massima quantità di lavoro svolto in un’unità di tempo), ma la prevedibilità della latenza. Detto in altre parole, ZGC garantisce pause quasi inesistenti, ma l’utilizzo continuo di risorse della CPU da parte del GC può rendere lenta complessivamente dell’applicazione.

I vantaggi principali sono:

  • Pause ultra-brevi: I tempi di stop-the-world (STW) non dipendono dalla dimensione dell’heap, ma dalla dimensione del “root set” (solitamente thread stacks).
  • Scalabilità: Gestisce heap fino a 16 TB.
  • Concorrenza: Quasi tutto il lavoro di GC (marcatura, compattazione, gestione dei riferimenti) avviene mentre i thread dell’applicazione sono in esecuzione.
  • Estremamente “self-tuning

Gli svantaggi principali sono:

  • Throughput inferiore: privilegia la latenza, non il throughput e di conseguenza si ha più CPU spesa per il GC e meno per l’applicazione da eseguire.
  • Overhead CPU elevato: ZGC è sempre attivo in background. Su macchine con pochi core può essere un problema.
  • Maggior uso di memoria
  • Non ideale per heap piccoli

Anche se G1 è il GC predefinito dalla versione 9 ed è un ottimo “tuttofare”, ci sono scenari specifici in cui ZGC è nettamente superiore.

  • Il limite dei 200ms (SLA di Latenza): G1 è progettato per tentare di rispettare un obiettivo di pausa (200ms è la latenza di default di G1), ma non lo garantisce. Sotto carichi pesanti o con heap molto grandi, le pause possono comunque schizzare a diverse centinaia di millisecondi o secondi. ZGC invece garantisce pause sotto il millisecondo (spesso nell’ordine dei microsecondi), indipendentemente dalla dimensione dell’heap.
  • Dimensione dell’Heap (Scalabilità): G1 inizia a soffrire quando l’heap supera i 32–64 GB, causando pause più lunghe. ZGC è nato per gestire heap colossali, da pochi gigabyte fino a 16 Terabyte

Nonostante i vantaggi, ZGC è da evitare se:

  1. Pochissima CPU: Poiché ZGC lavora costantemente in background, se il server è già al 90% di carico CPU, il GC finirà per rallentare pesantemente l’esecuzione del codice.
  2. Il Throughput è tutto: Se un processo che deve solo “macinare dati” e finire il prima possibile, e non importa se ogni tanto si ferma per mezzo secondo, G1 (o meglio ancora Parallel) finirà il lavoro in meno tempo totale.

SHENANDOAH

Shenandoah è un garbage collector (GC) a bassa pausa introdotto originariamente da Red Hat e disponibile in OpenJDK. Nella maggior parte dei GC tradizionali (come G1 o Parallel), la durata della pausa aumenta proporzionalmente alla quantità di dati “vivi” nell’heap. Se si ha 200 GB di dati, il tempo necessario per spostarli o scansionarli può causare lag di diversi secondi, rendendo l’applicazione lenta o non reattiva. Shenandoah risolve questo problema eseguendo quasi tutto il lavoro in parallelo all’applicazione.

Vantaggi:

  • Pause Prevedibili: Le pause sono solitamente nell’ordine dei millisecondi, sia che l’heap sia di 10 GB o di 500 GB.
  • Scalabilità: Ideale per applicazioni moderne che richiedono molta RAM (microservizi pesanti, database in-memory).

Svantaggi:

  • Overhead CPU: Poiché il GC lavora costantemente insieme all’app, consuma cicli di CPU che altrimenti sarebbero dedicati al codice business.
  • Throughput ridotto: Rispetto a un GC “Parallel”, l’applicazione potrebbe processare meno richieste totali al secondo, anche se è molto più reattiva.

Shenandoah vs ZGC

Spesso viene confrontato con ZGC (lo standard Oracle per la bassa latenza). Entrambi sono eccellenti, ma Shenandoah è spesso preferito in ambienti dove non si usa la JVM Oracle ufficiale (come le distribuzioni basate su OpenJDK di Red Hat o Amazon Corretto).

SpringSentinel – Maven Plugin Open Source Per L’analisi Statica Su Spring Boot

Sviluppare applicazioni con Spring Boot è veloce, ma garantire che il codice sia sempre performante, sicuro e allineato alle best practice non è altrettanto semplice. Ecco perché ho creato SpringSentinel, il plugin Maven per l’analisi statica che funge da vero e proprio “guardiano” per i tuoi progetti Java.

In questo articolo esploreremo come SpringSentinel può automatizzare l’audit del tuo codice.

Analizzare manualmente ogni riga di codice alla ricerca di query N+1 o vulnerabilità di sicurezza è un compito titanico. SpringSentinel automatizza questo processo focalizzandosi su tre pilastri fondamentali:

  1. Performance & Database: Rileva l’uso improprio di FetchType.EAGER e potenziali colli di bottiglia nelle transazioni.

  2. Sicurezza (Security): Scansiona il progetto alla ricerca di segreti hardcoded e configurazioni CORS insicure.

  3. Governance REST: Verifica che i tuoi endpoint siano versionati e seguano la convenzione kebab-case.

Per vedere SpringSentinel in azione, scoprire come leggere i report HTML e come configurarlo in meno di 2 minuti, guarda il video completo qui sotto:

 

Sul Futuro Della Programmazione – Risposta Post Linkedin

Video di risposta ad un post Linkedin. In breve, per me, la programmazione come era prima è quasi morta. Il futuro a breve termine sarà coordinare agenti, ottimizzazione token, creatore di app molto più potenti (paragonabili a piccole software house). Programmazione reale ridotta a poca roba.

Hibernate Performance: L’incubo dell’N+1 Select

Nello sviluppo di applicazioni enterprise con Spring Data JPA e Hibernate, esiste un confine sottile tra la comodità dell’astrazione e il disastro prestazionale. Questo confine è spesso marcato dall’N+1 Select, un problema che non genera errori di compilazione, non rompe i test unitari, ma può portare al collasso di un database in produzione sotto carichi reali.

jpa n+1 problem

Hibernate nasce per mappare oggetti su tabelle. Per efficienza, le relazioni (specialmente le collezioni come @OneToMany) sono impostate di default su FetchType.LAZY. Ciò significa che i dati correlati non vengono caricati finché non vi si accede esplicitamente.

Esempio

Immaginiamo un’entità Order che ha una lista di prodotti acquistati ItemOrder.  Quindi nel codice di Order avremo annotazione @OneToMany e in ItemOrder  @ManyToOne, con fetch type lazy di default, per non appesantire il sistema.

Se proviamo a recuperare gli ordini e accedere ai loro item, generiamo l’N+1:

// Query 1: Recupera tutti gli ordini (es. 10 ordini)

List<Order> orders = entityManager.createQuery(“SELECT o FROM Order o”, Order.class).getResultList();

for (Order order : orders) {

    // Query +N: Ogni volta che chiamiamo size() o iteriamo, Hibernate lancia una nuova query

    System.out.println(“Ordine: ” + order.getId() + ” – Item: ” + order.getItems().size());

}

 

A livello SQL, quello che accade dietro le quinte è disastroso:

  • Query 1: SELECT * FROM Orders; (Restituisce 10 righe)
  • Query 2: SELECT * FROM ItemOrder WHERE OrderId = 1;
  • Query 3: SELECT * FROM ItemOrder WHERE OrderId = 2;
  • … e così via fino alla decima query.

In totale abbiamo eseguito 1 + 10 = 11 query. Se avessi 1.000 ordini, l’applicazione eseguirebbe 1.001 query!  Se avessi N ordini avrei N+1 query ( da qui il nome del problema).

Ogni query al database comporta un “round-trip” (viaggio di andata e ritorno) sulla rete. Anche se la query singola è veloce, sommare centinaia di ritardi di rete crea una latenza enorme, saturando le connessioni disponibili e rallentando l’intera applicazione.

 

Soluzione 1: JOIN FETCH

La soluzione più comune e pulita in JPA è l’utilizzo della clausola JOIN FETCH. Questa istruzione dice a Hibernate di eseguire una JOIN SQL e di popolare immediatamente la collezione degli item.

String jpql = “SELECT o FROM Order o JOIN FETCH o.items”;
List<Order> orders = entityManager.createQuery(jpql, Order.class).getResultList();

// Ora questo ciclo NON genera altre query, i dati sono già in memoria
for (Order order : orders) {
System.out.println(“Item caricati: ” + order.getItems().size());
}

In genere, viene creata un query apposita in JPQL nel repository.

Soluzione 2: Entity Graph (JPA 2.1+)

Se non vuoi scrivere query JPQL personalizzate, puoi usare gli Entity Graphs. Questo approccio permette di definire “cosa” caricare a seconda del caso d’uso, mantenendo la query principale semplice.

EntityGraph<Order> graph = entityManager.createEntityGraph(Order.class);
graph.addSubgraph(“items”); // Specifichiamo di voler caricare anche la lista items

List<Order> orders = entityManager.createQuery(“SELECT o FROM Order o”, Order.class)
.setHint(“javax.persistence.loadgraph”, graph)
.getResultList();

Anche qui spesso, viene creata una query specifica con @EntityGraph nel repository