Java 14 JFR Event Streaming Tutorial with Examples

Introduction Java Flight Recorder (JFR) is a monitoring tool built into the JVM. It records events - garbage collections, thread locks, CPU load, memory allocation, exceptions, file and socket I/O, and events defined by the application - with an overhead low enough to keep it on in production. Until Java 14, though, JFR data could only be analyzed after a recording was written to a file, typically with JDK Mission Control. Java 14 (JEP 349) added JFR event streaming: a program can subscribe to events while they are being recorded, and react to them within about a second. This tutorial explains JFR event streaming with examples. We will start a recording stream, subscribe to garbage collection events, define a custom event, and read events back from a recording file.

(Note - This feature is part of Java 14Click to read tutorial: Overview of New Features in Java 14. JFR itself was open-sourced in Java 11Click to read: Java 11.) The streaming API The API lives in the jdk.jfr.consumer package of the jdk.jfr module -
  • RecordingStream - starts a new recording in the current JVM and streams its events. You choose events with enable(eventName), optionally with settings such as withPeriod() or withThreshold().
  • onEvent(name, action) - registers a callback, a Consumer<RecordedEvent>, for one type of event. RecordedEvent gives access to the event's fields by name, its start time and its duration.
  • start() runs the stream on the current thread until it is closed; startAsync() runs it in the background.
  • EventStream.openRepository() streams events from another JVM's recording on the same machine, and EventStream.openFile() reads a recording file.
Streaming built-in and custom events The program below streams two kinds of events: the JVM's built-in jdk.GarbageCollection event, and a custom OrderEvent defined by the application. A custom event is a class which extends jdk.jfr.Event; its fields become the event's fields.
Subscribing to JFR events with RecordingStream
//JfrStreamingDemo.java
import java.util.concurrent.atomic.AtomicInteger;
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;
import jdk.jfr.consumer.RecordingStream;

public class JfrStreamingDemo {

  @Name("com.javabrahman.Order")
  @Label("Order Placed")
  static class OrderEvent extends Event {
    @Label("Item") String item;
    @Label("Amount") double amount;
  }

  public static void main(String[] args) throws Exception {
    AtomicInteger gcCount = new AtomicInteger();
    try (RecordingStream stream = new RecordingStream()) {
      stream.enable("jdk.GarbageCollection");
      stream.enable("com.javabrahman.Order");

      stream.onEvent("com.javabrahman.Order", e -> System.out.printf(
          "Order event: %s for %.2f (took at least 20 ms: %b)%n",
          e.getString("item"), e.getDouble("amount"), e.getDuration().toMillis() >= 20));
      stream.onEvent("jdk.GarbageCollection", e -> {
        if (gcCount.incrementAndGet() == 1)
          System.out.println("GC event: " + e.getString("name") + ", cause: " + e.getString("cause"));
      });
      stream.startAsync();                     // stream in the background

      for (String item : new String[] {"Book", "Pen"}) {
        OrderEvent event = new OrderEvent();
        event.begin();                         // start timing the event
        Thread.sleep(20);                      // stands in for real work
        event.item = item;
        event.amount = item.length() * 10.5;
        event.commit();                        // record it
      }
      System.gc();                             // trigger a garbage collection event
      Thread.sleep(2500);                      // give the stream time to deliver the events
    }
    System.out.println("Saw GC events: " + (gcCount.get() > 0));
  }
}
 OUTPUT of the above code
Order event: Book for 42.00 (took at least 20 ms: true)
Order event: Pen for 31.50 (took at least 20 ms: true)
GC event: G1Full, cause: System.gc()
Saw GC events: true
Explanation of the code
  • new RecordingStream() starts a recording in the same JVM. Only the enabled events are recorded, which keeps the overhead low.
  • OrderEvent is an ordinary class with JFR annotations: @Name gives the event's unique name, used in enable() and onEvent(), and @Label gives readable names shown in tools like JDK Mission Control.
  • begin() and commit() mark the start and end of the event, so JFR records its duration as well as its fields. If the event type is not enabled, commit() does almost nothing, so instrumented code costs little when nobody is listening.
  • System.gc() causes a full collection, reported by the default G1 collector as G1Full with the cause System.gc().
  • Events are written to an in-memory buffer and flushed about once a second, so they reach the callbacks with a short delay. That is why the program waits before closing the stream.
Reading a recording file JFR recordings are usually saved to .jfr files, either from code with the Recording class, or by starting the JVM with -XX:StartFlightRecording. The consumer API can read them back -
Recording to a file and reading it back
//JfrFileDemo.java
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.TreeMap;
import jdk.jfr.Recording;
import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordingFile;

public class JfrFileDemo {
  public static void main(String[] args) throws Exception {
    Path file = Files.createTempFile("app", ".jfr");
    try (Recording recording = new Recording()) {
      recording.enable("jdk.GarbageCollection");
      recording.enable("jdk.ThreadSleep");
      recording.start();
      System.gc();
      Thread.sleep(50);
      recording.stop();
      recording.dump(file);
    }
    Map<String, Integer> counts = new TreeMap<>();
    for (RecordedEvent event : RecordingFile.readAllEvents(file)) {
      counts.merge(event.getEventType().getName(), 1, Integer::sum);
    }
    System.out.println("Event types in the file: " + counts.keySet());
    Files.delete(file);
  }
}
 OUTPUT of the above code
Event types in the file: [jdk.GarbageCollection, jdk.ThreadSleep]
The same file could be opened in JDK Mission Control, or summarized on the command line with the jfr tool, for example jfr summary app.jfr or jfr print --events jdk.GarbageCollection app.jfr. What event streaming is used for
  • Continuous monitoring: pushing selected JFR metrics, such as GC pause times or lock contention, to a monitoring system like Prometheus, without an external agent.
  • Alerting: reacting immediately when an event crosses a threshold, for example logging a warning when a GC pause exceeds 100 ms.
  • Application metrics: custom events like OrderEvent give business-level timings with very low overhead, and appear alongside the JVM's own events.
Later releases kept improving JFR - Java 25, for example, added method timing and tracing, and experimental CPU-time profiling on Linux. Summary In this tutorial we saw how Java 14's JFR event streaming lets a program subscribe to Java Flight Recorder events while they are recorded. We used RecordingStream to enable events and register onEvent() callbacks, received a built-in garbage collection event, and defined and timed a custom event by extending jdk.jfr.Event. We then saved a recording to a file and read it back with RecordingFile, and lastly looked at what event streaming is used for. Next, read the overview of new features in Java 15Click to read tutorial: Overview of New Features in Java 15.