
Spring Boot è una delle piattaforme più popolari per lo sviluppo di applicazioni Java, ma uno dei suoi principali svantaggi è il tempo di avvio. Project Leyden è un progetto open source che punta a ridurlo senza rinunciare al modello di esecuzione della JVM.
In questo articolo vedremo brevemente le differenze tra GraalVM e Project Leyden, per poi passare alla pratica e capire come usare Project Leyden per migliorare i tempi di avvio di un'applicazione Spring Boot che integra servizi AWS.
Metriche JVM
Startup Time
È il tempo necessario per completare il primo compito (task) dell'applicazione. Include il caricamento delle classi,
il linking e l'esecuzione dei blocchi di inizializzazione statica (
Time to Peak
Java non raggiunge la velocità massima istantaneamente. Il Peak Performance è il livello massimo di throughput
che l'app può sostenere. Il "Time to Peak" è il tempo necessario affinché la maggior parte del codice critico sia stata compilata in Tier 4,
il livello di ottimizzazione massima del compilatore C2.
Speculazione e De-ottimizzazione
Questa è la vera "magia" della JVM: il compilatore JIT fa delle scommesse (speculazioni). Ad esempio, se vede che
un'interfaccia ha una sola implementazione durante il profiling, compila il codice assumendo che sarà sempre così.
Se in seguito carichi una nuova classe che rompe questa assunzione, la JVM esegue una de-ottimizzazione: scarta il codice veloce e torna
all'interprete per ricalcolare la strategia.
Funzionamento standard di una app JVM
Quando avviamo un'applicazione Java, inizia un processo a più stadi chiamato Tiered Compilation:
T0 - Interprete (Lento): La JVM legge il bytecode Java, istruzione per istruzione, e lo traduce "al volo" in istruzioni per la CPU. È la fase più lenta, ma permette all'app di partire immediatamente senza aspettare compilazioni.
T1-T3 - C1 JIT (Veloce): Appena un metodo diventa "caldo", il compilatore C1 lo trasforma in codice nativo con ottimizzazioni leggere. Mentre esegue questo codice, la JVM osserva contemporaneamente quali metodi vengono chiamati più spesso e quali tipi di dati fluiscono attraverso il codice: queste osservazioni sono chiamate "profili" e serviranno al passo successivo.
T4 - C2 JIT (Massima velocità): Quando il codice è molto usato, interviene il compilatore C2. Usa i dati di profiling raccolti da C1 per applicare ottimizzazioni estreme (come l'inlining o il loop unrolling).
Questo processo non è a senso unico: la JVM può tornare a stadi precedenti se le sue assunzioni si rivelano errate (speculazione e de-ottimizzazione).
AOT con GraalVM
GraalVM è una tecnologia che consente di compilare un'applicazione Java in un eseguibile nativo ahead-of-time (AOT), eliminando la necessità di una JVM a runtime e riducendo drasticamente i tempi di avvio.
Il cuore di questa tecnologia è Native Image: invece di compilare il codice "Just-In-Time" mentre l'app gira, GraalVM esegue tutto il lavoro pesante durante la fase di build:
- Scansione: GraalVM analizza tutto il codice, le dipendenze e il codice della JDK stessa.
- Analisi di raggiungibilità: Identifica solo il codice che viene effettivamente utilizzato e scarta tutto il resto.
- Risultato: Viene generato un eseguibile specifico per il sistema operativo, che non richiede una JVM esterna per girare.
La filosofia alla base di questo approccio è la "Closed World Assumption": tutto il codice raggiungibile deve essere noto a compile-time. Questo significa che tutte le classi, i metodi e le risorse utilizzati dall'applicazione devono essere specificati esplicitamente, altrimenti potrebbero non essere inclusi nell'immagine nativa, causando errori a runtime.
Di conseguenza, funzionalità dinamiche come reflection, dynamic proxies e class loading dinamico non funzionano
automaticamente e richiedono configurazione esplicita (ad esempio tramite il file reflection-config.json). Questo
rappresenta una limitazione concreta per molte applicazioni Java che ne fanno uso intensivo, come quelle basate su
Spring Boot.
Infine, spostare tutto il lavoro al momento della build comporta tempi di compilazione significativamente più lunghi rispetto all'approccio JIT. Questi compromessi sono però accettabili in contesti dove startup istantaneo e footprint minimo sono prioritari, come microservizi e funzioni serverless.
AOT con Project Leyden
Il problema della JVM standard è che tutto il lavoro di caricamento, profiling e compilazione ricomincia da zero a ogni riavvio. A differenza di GraalVM, che risolve questo problema eliminando la JVM a runtime, Project Leyden adotta un approccio diverso: ottimizzare la JVM stessa, preservando tutto il dinamismo che la rende potente. Se GraalVM si basa sulla "Closed World Assumption", Project Leyden segue invece una logica di Open World: l'applicazione continua a girare dentro una JVM completa, mantenendo caratteristiche come loading dinamico, e reflection.
Per farlo, Leyden introduce il concetto di Training Run: un'esecuzione preliminare dell'applicazione in cui la JVM raccoglie dati di profiling e compila il codice critico. Il risultato viene salvato in un'AOT Cache: un archivio che contiene classi pre-caricate, metodi già compilati in codice nativo e lo stato dell'heap pre-inizializzato. Ad ogni avvio successivo, la JVM attinge direttamente da questa cache invece di ricominciare da zero. Il processo si articola in tre fasi:
- Spostare il calcolo (Shifting): Il profiling e la compilazione JIT vengono eseguiti durante il Training Run, invece che a ogni avvio.
- Catturare lo stato: Il codice già compilato e le classi già linkate vengono salvati nell'AOT Cache.
- Deployment fulmineo: Al riavvio (Deployment Run), l'app carica direttamente il codice ottimizzato dall'AOT Cache, riducendo drasticamente il Time to Peak.
Possiamo vedere la JVM standard come un atleta che deve fare riscaldamento ogni mattina, Leyden permette all'atleta di "salvare" il riscaldamento del giorno prima e iniziare subito a correre alla massima velocità.
Project Leyden in pratica
Per testare Project Leyden con un'applicazione Spring Boot useremo uno dei miei progetti GitHub (https://github.com/vincenzo-racca/localstack), in cui Spring Boot si integra con servizi AWS come SQS e DynamoDB.
La versione utilizzata di Spring Boot è la 3.5.13; la macchina usata per i test è una macchina virtuale Ubuntu Server 25 con 1 vCPU e 4 GB di RAM.
Per ogni test è stata generata l'immagine container dell'applicazione con il comando:
./mvnw clean spring-boot:build-image
Successivamente è stato eseguito docker compose up -d per avviare i container di Localstack (servizi AWS in locale) e
l'applicazione Spring Boot containerizzata.
Primo test: JVM standard con Java 21
Applicazione avviata in 4,136 secondi:

Secondo test: CDS con Java 21
Per abilitare CDS, basta aggiungere questa configurazione al pom.xml:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<env>
<BP_JVM_CDS_ENABLED>true</BP_JVM_CDS_ENABLED>
</env>
</image>
</configuration>
</plugin>
Inoltre, dato che la generazione dell'immagine include automaticamente la fase di training,
è fondamentale che l'app riesca ad avviarsi in ambiente containerizzato; per questo è stato aggiunto il prefisso optional: alla property nel file application.properties:
spring.config.import=optional:aws-parameterstore:/config/localstack/?prefix=localstack.
Applicazione avviata in 1,119 secondi:

Terzo test: Project Leyden con Java 25
Per beneficiare di Project Leyden, è necessario aggiornare Java alla versione 24 o successiva (in questo caso ho usato la 25, attuale LTS). Di seguito i benefici ottenuti con Project Leyden:
| Funzionalità | Java 21 + CDS | Java 24+ + Leyden |
|---|---|---|
| Classi pre-caricate | ✅ | ✅ |
| Codice JIT salvato | ❌ | ✅ |
| Heap pre-inizializzato | ❌ | ✅ |
| Riduzione startup | ✅ | ✅ |
| Riduzione time to peak | ❌ | ✅ |
Applicazione avviata in 0,776 secondi:

Conclusioni
Nei test di questo articolo, il salto più interessante è già visibile con CDS: si passa da 4,136 secondi di startup con la JVM standard a 1,119 secondi su Java 21. Con Project Leyden su Java 25 si scende ulteriormente a 0,776 secondi, ottenendo quindi un miglioramento molto concreto senza dover riscrivere l'applicazione e senza rinunciare al dinamismo tipico della JVM.
In pratica, GraalVM resta la scelta più aggressiva quando l'obiettivo principale è minimizzare startup e footprint, ma Project Leyden si presenta come l'opzione più pragmatica per molte applicazioni Spring Boot: zero cambiamenti, integrazione naturale con il runtime Java e vantaggi sia sul tempo di avvio sia sul time to peak. È una tecnologia ancora in evoluzione, ma i risultati mostrano già chiaramente che vale la pena iniziare a sperimentarla.
Riferimenti
- https://openjdk.org/projects/leyden/
- https://openjdk.org/projects/leyden/slides/leyden-heidinga-devnexus-2024-03.pdf
- https://openjdk.org/projects/leyden/slides/leyden-jvmls-2023-08-08.pdf
Altri articoli su Spring: Spring.
Mio libro Spring Boot 3 API Mastery su Amazon: https://amzn.to/4bU4BNS

