Contents
- JFR Architecture Overview
- Starting a Recording
- Controlling JFR with jcmd
- Programmatic Recording API
- Custom JFR Events
- Live Event Streaming with RecordingStream
- Analysing Recordings with JMC
- JFR Configuration Profiles
JFR works as a circular in-memory buffer inside the JVM. Events are written asynchronously without allocating on the Java heap. When the buffer fills or a dump is requested, data is flushed to a .jfr binary file. Key properties:
- ~1% overhead — suitable for always-on recording in production with the continuous profile
- ~500+ built-in event types — GC, JIT, threads, sockets, file I/O, exceptions, class loading, CPU, heap statistics
- Custom events — annotate a POJO that extends jdk.jfr.Event and commit it from any code path
- Live streaming — RecordingStream (Java 14+) delivers events with ~1 s latency
JFR requires no external agent or profiler. It is part of the JDK — available in both OpenJDK and Oracle JDK since Java 11 at no cost.
The simplest way is a JVM flag at startup:
# Start recording immediately, save to file when JVM exits
java -XX:StartFlightRecording=filename=app.jfr,dumponexit=true,settings=default \
-jar myapp.jar
# Always-on continuous recording (overwrites old data — good for production)
java -XX:StartFlightRecording=name=continuous,maxage=1h,maxsize=250m,settings=profile \
-jar myapp.jar
# Two built-in settings profiles:
# default — low overhead (~1%), suited for always-on
# profile — higher overhead (~2%), more detailed method profiling
Two built-in settings: default (minimal overhead, production-safe) and profile (richer data including method sampling at 10 ms). Custom settings can be created with JMC's template editor.
jcmd lets you start, stop, dump, and query recordings on a running JVM without restarting.
# Find the PID of your running Java process
jcmd # lists all JVM processes
# Start a recording on PID 12345
jcmd 12345 JFR.start name=myRecording settings=profile maxage=5m
# Dump current buffer to file (recording continues)
jcmd 12345 JFR.dump name=myRecording filename=/tmp/dump.jfr
# Stop recording and write to file
jcmd 12345 JFR.stop name=myRecording filename=/tmp/final.jfr
# List all active recordings
jcmd 12345 JFR.check
# Check a specific recording
jcmd 12345 JFR.check name=myRecording verbose=true
The jdk.jfr.Recording API (Java 9+) lets you start, configure, and dump recordings from within your application — useful for recording only during specific operations.
import jdk.jfr.*;
import java.nio.file.Path;
import java.time.Duration;
// Start a scoped recording around a specific operation
public void runWithRecording(Runnable operation) throws Exception {
Configuration config = Configuration.getConfiguration("profile");
try (Recording recording = new Recording(config)) {
recording.setName("operation-profile");
recording.setMaxAge(Duration.ofMinutes(10));
recording.setMaxSize(64 * 1024 * 1024); // 64 MB
recording.setDumpOnExit(false);
// Enable specific event categories
recording.enable("jdk.GarbageCollection").withThreshold(Duration.ofMillis(10));
recording.enable("jdk.SocketRead").withThreshold(Duration.ofMillis(20));
recording.enable("jdk.FileRead").withStackTrace();
recording.start();
try {
operation.run(); // your measured code
} finally {
recording.stop();
recording.dump(Path.of("/tmp/recording-" + System.currentTimeMillis() + ".jfr"));
}
}
}
// List all available event types programmatically
FlightRecorder.getFlightRecorder()
.getEventTypes()
.stream()
.filter(et -> et.getCategoryNames().contains("Java Virtual Machine"))
.map(EventType::getName)
.forEach(System.out::println);
Extend jdk.jfr.Event and annotate fields. The JVM registers the event type automatically when the class is loaded.
import jdk.jfr.*;
// Define the event schema
@Name("com.myapp.OrderProcessed")
@Label("Order Processed")
@Description("Fired when an order completes processing")
@Category({"MyApp", "Orders"})
@StackTrace(false) // omit stack trace to reduce overhead
public class OrderProcessedEvent extends Event {
@Label("Order ID")
public String orderId;
@Label("Total Amount")
@DataAmount // hint: this is a monetary/byte amount
public double totalAmount;
@Label("Item Count")
public int itemCount;
@Label("Processing Time Ms")
@Timespan(Timespan.MILLISECONDS)
public long processingTimeMs;
}
// Emit the event
public Order processOrder(String orderId, List<Item> items) {
OrderProcessedEvent event = new OrderProcessedEvent();
event.begin(); // start the built-in duration timer
try {
Order order = doProcess(orderId, items);
event.orderId = orderId;
event.totalAmount = order.getTotal();
event.itemCount = items.size();
event.processingTimeMs = /* elapsed */;
if (event.shouldCommit()) { // avoid overhead if JFR is not recording
event.commit();
}
return order;
} catch (Exception e) {
event.orderId = orderId;
event.commit();
throw e;
}
}
event.shouldCommit() returns false when JFR is not running or the event's threshold is not met, letting you leave event code in production with zero overhead when not recording.
RecordingStream (Java 14+) delivers events to handlers in near-real time — useful for live dashboards, alerts, and custom monitoring agents without writing to disk.
import jdk.jfr.consumer.RecordingStream;
import java.time.Duration;
// Stream events to a live handler
try (RecordingStream rs = new RecordingStream()) {
// Configure which events to stream
rs.enable("jdk.GarbageCollection").withThreshold(Duration.ofMillis(100));
rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
rs.enable("com.myapp.OrderProcessed");
// Handle GC events
rs.onEvent("jdk.GarbageCollection", event -> {
System.out.printf("GC: %s — %.0f ms%n",
event.getString("cause"),
event.getDuration().toMillis() * 1.0);
});
// Handle CPU load
rs.onEvent("jdk.CPULoad", event -> {
float jvmUser = event.getFloat("jvmUser");
if (jvmUser > 0.80f) {
System.out.println("HIGH CPU: " + (jvmUser * 100) + "%");
}
});
// Handle custom order events
rs.onEvent("com.myapp.OrderProcessed", event -> {
System.out.printf("Order %s: %.2f — %d items%n",
event.getString("orderId"),
event.getDouble("totalAmount"),
event.getInt("itemCount"));
});
rs.startAsync(); // runs in background thread
// ... application continues running ...
Thread.sleep(Long.MAX_VALUE);
}
Java Mission Control (JMC) is the GUI tool for analysing .jfr files. Download it from jdk.java.net/jmc. Key views:
- Automated Analysis — JMC's rules engine flags issues automatically: excessive GC, thread contention, hot methods, memory leaks
- Method Profiling — flame graph of sampled CPU time — find hot methods without modifying code
- Heap Statistics — object allocation by type, TLAB statistics, old-gen pressure
- Thread View — timeline of thread states (RUNNABLE, BLOCKED, WAITING) per thread — spot lock contention
- GC Detail — pause times, GC cause, heap occupancy before/after
- I/O View — socket read/write and file read/write latencies
- Event Browser — raw event table with filtering — view your custom events here
# Parse a recording from the command line (Java 17+ — no JMC needed)
# Print event types in a recording
java -cp jfr-tool.jar jdk.jfr.tool.Main print --events jdk.GarbageCollection app.jfr
# Built-in jfr tool (Java 14+)
jfr print --events jdk.GarbageCollection app.jfr
jfr print --events com.myapp.OrderProcessed app.jfr
jfr summary app.jfr # high-level statistics
JFR settings are XML files. You can tune exactly which events are captured and at what thresholds.
<!-- custom-profile.jfc -- save as src/main/resources/custom-profile.jfc -->
<configuration version="2.0" label="Custom App Profile"
description="Low overhead profile with custom app events"
provider="MyOrg">
<!-- Turn on method profiling at 20ms sample interval -->
<event name="jdk.ExecutionSample">
<setting name="enabled">true</setting>
<setting name="period">20 ms</setting>
</event>
<!-- Record only GC pauses longer than 50ms -->
<event name="jdk.GarbageCollection">
<setting name="enabled">true</setting>
<setting name="threshold">50 ms</setting>
</event>
<!-- Always capture custom order events -->
<event name="com.myapp.OrderProcessed">
<setting name="enabled">true</setting>
<setting name="threshold">0 ms</setting>
</event>
<!-- Disable noisy low-value events -->
<event name="jdk.ClassLoad">
<setting name="enabled">false</setting>
</event>
</configuration>
# Use the custom configuration
java -XX:StartFlightRecording=settings=custom-profile.jfc,filename=app.jfr -jar myapp.jar
Embed your .jfc configuration file in your JAR (src/main/resources) so it travels with the application. Reference it by classpath path or extract it to a temp file at startup with ClassLoader.getResourceAsStream().