Contents

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:

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:

# 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().