Quando si parla delle novità introdotte con Java 11 vengono spesso citati nuovi metodi della classe String, il nuovo HttpClient o alcuni miglioramenti alla JVM. Esiste però una funzionalità molto più interessante, soprattutto per chi sviluppa applicazioni backend: Java Flight Recorder, abbreviato in JFR.
Il nome non è casuale. Il concetto è molto simile a quello della scatola nera di un aereo: mentre un’applicazione Java è in esecuzione, la JVM può registrare una grande quantità di informazioni sul proprio comportamento. Thread, memoria, Garbage Collector, lock, eccezioni, utilizzo della CPU, allocazioni di oggetti e molte altre informazioni possono essere raccolte e analizzate successivamente.
La cosa particolarmente interessante è che tutto questo può essere fatto senza modificare il codice dell’applicazione.
Java Flight Recorder non nasce realmente con Java 11
Una precisazione è importante. Flight Recorder esisteva già prima di Java 11. La grande novità arrivata con Java 11 è rappresentata dal JEP 328, con il quale Flight Recorder è stato portato nell’ecosistema OpenJDK.
Da quel momento JFR è diventato uno degli strumenti più interessanti disponibili direttamente nella piattaforma Java per effettuare diagnostica e profiling delle applicazioni.
Non stiamo quindi parlando semplicemente di una nuova API, ma di uno strumento integrato profondamente nella JVM.
Ma cosa registra esattamente?
Durante l’esecuzione di un’applicazione, Java Flight Recorder raccoglie degli eventi. Ogni evento descrive qualcosa che è avvenuto all’interno della JVM o dell’applicazione.
CPU
│
├── Thread
│
├── Garbage Collection
│
├── Allocazioni di memoria
│
├── Lock e sincronizzazione
│
├── File I/O
│
├── Socket I/O
│
├── Eccezioni
│
├── Class Loading
│
└── Attività della JVM
Questo significa che davanti a un’applicazione che improvvisamente diventa lenta non siamo più costretti a basarci esclusivamente sui log o su supposizioni. Possiamo osservare ciò che è realmente successo all’interno della JVM.
Registrare un’applicazione è sorprendentemente semplice
Supponiamo di avere una normale applicazione Java distribuita come file JAR. Con Java 11 possiamo avviarla chiedendo contemporaneamente alla JVM di effettuare una registrazione:
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr \
-jar applicazione.jar
Con questo comando stiamo dicendo alla JVM:
Avvia l'applicazione
│
▼
Registra ciò che accade
│
▼
Continua per 60 secondi
│
▼
Salva tutto in recording.jfr
Al termine avremo quindi un file:
recording.jfr
che contiene gli eventi raccolti durante quel minuto di esecuzione. L’opzione -XX:StartFlightRecording è documentata direttamente tra le opzioni della JVM di Java 11 e permette di configurare durata, nome del file, profilo di registrazione e altri parametri.
Possiamo registrare anche una JVM già avviata
Qui la faccenda diventa ancora più interessante. Immaginiamo che un backend sia già in esecuzione e improvvisamente inizi a mostrare problemi di performance. Non è obbligatorio riavviarlo.
Possiamo individuare il PID del processo Java:
jcmd
e supponiamo di ottenere:
18342 com.cutalab.Application
Possiamo quindi avviare una registrazione direttamente sul processo:
jcmd 18342 JFR.start name=analisi settings=profile duration=60s filename=analisi.jfr
In altre parole, possiamo collegarci a una JVM già attiva e dirle: registrami cosa sta succedendo adesso.
Ed entra in scena JDK Mission Control
Il file .jfr è solo la registrazione grezza. Per analizzarlo possiamo utilizzare JDK Mission Control, spesso abbreviato semplicemente in JMC.
Mission Control permette di trasformare migliaia di eventi della JVM in grafici, timeline, stack trace e analisi molto più semplici da interpretare.

Un backend lento: dove stiamo perdendo tempo?
Facciamo un esempio concreto. Abbiamo un servizio REST che normalmente risponde in circa 50 millisecondi. Improvvisamente alcune richieste iniziano a richiedere quasi un secondo.
Dai normali log potremmo vedere qualcosa del genere:
GET /api/product/42
Response: 200
Execution time: 924 ms
Sappiamo che la richiesta è lenta, ma non sappiamo necessariamente perché.
Analizzando la registrazione JFR potremmo invece arrivare a una situazione come:
HTTP Request
│
▼
Controller
│
▼
Service
│
├── Database ........ 18 ms
│
├── JSON ............. 5 ms
│
└── WAITING ......... 821 ms
▲
│
IL PROBLEMA
A quel punto sappiamo che il problema non è il database e non è la serializzazione JSON. Un thread sta trascorrendo gran parte del proprio tempo in attesa.
Scoprire thread bloccati e lock
Uno degli utilizzi più interessanti di JFR riguarda proprio la sincronizzazione. Mission Control può mostrare quali thread sono rimasti bloccati, per quanto tempo e soprattutto quale lock stavano tentando di acquisire.

Nell’esempio mostrato dalla documentazione Oracle è possibile arrivare fino allo stack trace dei thread coinvolti. Questo permette di passare da una generica affermazione come:
"L'applicazione ogni tanto è lenta"
a qualcosa di decisamente più utile:
Worker Thread
│
▼
Logger.log()
│
▼
attesa sul monitor
│
▼
thread bloccato
Garbage Collector sotto osservazione
Un’altra causa molto comune di problemi nelle applicazioni Java è il Garbage Collector. Un’applicazione può sembrare lenta non perché il codice sia necessariamente inefficiente, ma perché la JVM sta effettuando troppe operazioni di garbage collection o pause troppo lunghe.
Java Flight Recorder registra anche questi eventi.
Possiamo quindi osservare le pause del GC, la pressione sulla memoria, le allocazioni e comprendere quali classi stanno producendo più oggetti.
Un piccolo esperimento
Possiamo creare volutamente un programma poco efficiente per vedere JFR al lavoro. Per esempio:
import java.util.ArrayList;
import java.util.List;
public class MemoryTest {
public static void main(String[] args) throws Exception {
List<String> dati = new ArrayList<>();
for (int i = 0; i < 5_000_000; i++) {
dati.add(
"Oggetto temporaneo numero " + i
);
if (i % 100_000 == 0) {
Thread.sleep(20);
}
}
System.out.println(
"Elementi creati: " + dati.size()
);
}
}
Compiliamo:
javac MemoryTest.java
e avviamo il programma con Flight Recorder:
java -XX:StartFlightRecording=filename=test.jfr,settings=profile MemoryTest
Aprendo test.jfr in Mission Control potremo osservare le allocazioni di memoria generate dal programma, l’attività del Garbage Collector e i metodi maggiormente coinvolti.
Quanto costa tutto questo in termini di performance?
Questa è probabilmente la domanda più importante. Un profiler tradizionale può influenzare l’applicazione che sta misurando. Java Flight Recorder è invece stato progettato fin dall’inizio per avere un overhead molto contenuto.
La documentazione Oracle relativa a Java 11 indica che una normale registrazione di profiling con le impostazioni standard rimane generalmente sotto il 2% di overhead nella maggior parte delle applicazioni, mentre una registrazione continua standard può avere un impatto non misurabile in molti casi.
Naturalmente esistono eventi più costosi da raccogliere e una configurazione aggressiva può aumentare l’overhead. JFR non significa quindi “costo sempre zero”, ma è stato progettato proprio pensando anche agli ambienti di produzione.
Default e Profile
Java Flight Recorder mette a disposizione diverse configurazioni. Due concetti importanti da conoscere sono default e profile.
default
│
├── meno eventi
├── overhead molto basso
└── adatto anche a registrazioni lunghe
profile
│
├── più informazioni
├── maggiore dettaglio
└── ideale per analisi brevi
Per una registrazione diagnostica breve possiamo quindi utilizzare:
java -XX:StartFlightRecording=duration=2m,settings=profile,filename=profiling.jfr \
-jar applicazione.jar
La cosa più interessante: possiamo creare i nostri eventi
JFR non è limitato agli eventi prodotti internamente dalla JVM. È possibile definire anche eventi applicativi personalizzati.
Per esempio potremmo voler misurare un’operazione particolarmente importante del nostro backend.
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;
@Name("cutalab.ProductRequest")
@Label("Product Request")
public class ProductRequestEvent extends Event {
@Label("Product ID")
String productId;
}
Nel nostro service potremmo quindi scrivere:
ProductRequestEvent event = new ProductRequestEvent();
event.productId = "42";
event.begin();
try {
productService.loadProduct("42");
} finally {
event.end();
event.commit();
}
Da quel momento quella particolare operazione può comparire all’interno della registrazione JFR insieme agli eventi della JVM.
È una caratteristica estremamente potente perché permette di correlare gli eventi dell’applicazione con ciò che stava avvenendo nello stesso momento nella JVM.
Dai log alla telemetria della JVM
I log continuano naturalmente ad essere fondamentali, ma raccontano soltanto ciò che abbiamo deciso preventivamente di scrivere.
LOGGER.info("Loading product {}", id);
Se non abbiamo inserito il log giusto nel punto giusto, quell’informazione non esisterà.
Flight Recorder affronta il problema da una prospettiva completamente diversa: raccoglie gli eventi del runtime.
APPLICAZIONE
│
┌──────────┴──────────┐
│ │
▼ ▼
LOG JFR
│ │
ciò che decidiamo ciò che accade
di registrare nella JVM
Perché Java Flight Recorder è una delle funzionalità più interessanti di Java 11
Java 11 ha introdotto numerose migliorie importanti, ma Java Flight Recorder rappresenta qualcosa di diverso.
Non modifica semplicemente il modo in cui scriviamo una riga di codice: modifica il modo in cui possiamo osservare un’applicazione Java mentre sta funzionando.
Davanti a un backend lento possiamo registrare la JVM, riprodurre il problema e analizzare successivamente cosa è realmente successo.
Problema in produzione
│
▼
Flight Recording
│
▼
file .jfr
│
▼
JDK Mission Control
│
▼
Thread / CPU / Memory / GC / Lock / I/O
│
▼
causa del problema
Ed è proprio per questo che il nome Flight Recorder è particolarmente azzeccato.
Quando qualcosa va storto non dobbiamo necessariamente cercare di indovinare cosa sia successo.
Possiamo aprire la scatola nera della JVM e guardare.