Last week, I showed you the Fastest Way to get the Version of a Java Installation. This week, I’ll show you something completely different: how to interact with JFR data programmatically, showcasing a new library called basic-jfr-processor in the process.
While JFR is a great tool for profiling your application and gaining insights, the file format is, on purpose, not well documented or specified. One of the best sources of information is Gunnar Morling’s blog post on the topic, and of course, the OpenJDK source code.
But of course, there are ready-made APIs for reading JFR files and OpenJDK-adjacent libraries to write them. In this overview blog post, I’ll showcase the built-in Java JFR API, Jaroslav Bachorik’s jafar API, and the JMC JFR writer API, as well as my own basic-jfr-processor library based on the latter.
We start with the built-in API:
Reading JFR Files using Java’s API
The API is pretty simple: it consists of a RecordingFile class that allows us to parse the events in the file as RecordingEvent instances with properties that can be a primitive, a special type like RecordedStackTrace or RecordedClass, or a complex RecordingObject:
The following is an example from documentation: It prints a histogram of all sampled methods in a file:
public static void main(String[] args) throws IOException {
if (args.length != 1) {
System.err.println("Must specify a recording file.");
return;
}
RecordingFile.readAllEvents(Path.of(args[0])).stream()
.filter(e -> e.getEventType().getName().equals("jdk.ExecutionSample"))
.map(e -> e.getStackTrace())
.filter(s -> s != null)
.map(s -> s.getFrames().getFirst())
.filter(f -> f.isJavaFrame())
.map(f -> f.getMethod())
.collect(
Collectors.groupingBy(m -> m.getType().getName() + "." + m.getName() + " " + m.getDescriptor(),
Collectors.counting()))
.entrySet()
.stream()
.sorted((a, b) -> b.getValue().compareTo(a.getValue()))
.forEach(e -> System.out.printf("%8d %s\n", e.getValue(), e.getKey()));
// there is also an iterator-like API via new RecordingFile(outputFile)
}
You can access fields of an event or object by using one of the many accessor functions, like getBoolean(String name). The main advantage of this API is that it’s already built in and maintained by the JFR authors themselves. The main disadvantage is that it’s a pull-based API (we ask JFR for the next event) and that it’s rather slow.
An alternative API is jafar by Jaroslav Bachorik:
Reading JFR Files using Jafar
Jafar allows us to directly parse JFR files into objects, removing the need for slow object-getter requests like in the previous API and making the code far more readable.
The following is an example from the documentation:
import io.jafar.parser.api.*;
import java.nio.file.Paths;
@JfrType("custom.MyEvent")
public interface MyEvent { // no base interface required
String myfield();
}
try (TypedJafarParser p = JafarParser.newTypedParser(Paths.get("/path/to/recording.jfr"))) {
HandlerRegistration<MyEvent> reg = p.handle(MyEvent.class, (e, ctl) -> {
System.out.println(e.myfield());
long pos = ctl.stream().position(); // current byte position while in handler
// ctl.abort(); // optionally stop parsing immediately without throwing
});
p.run();
reg.destroy(p); // deregister
}
This API is especially great when parsing large JFR files with known events, as jafar uses compile-time code generation to improve speed and drastically reduce memory footprint (more on this in the excellent README). But jafar also has an untyped API (again, the example is from the documentation):
import io.jafar.parser.api.*;
import java.nio.file.Paths;
try (UntypedJafarParser p = JafarParser.newUntypedParser(Paths.get("/path/to/recording.jfr"))) {
HandlerRegistration<?> reg = p.handle((type, value) -> {
if ("jdk.ExecutionSample".equals(type.getName())) {
// You can retrieve the value by providing 'path' -> "eventThread", "javaThreadId"
Object threadId = Values.get(value, "eventThread", "javaThreadId");
// You can also get the value conveniently typed - for primitive values you need to use the boxed type in the call
long threadIdLong = Values.as(value, Long.class, "eventThread", "javaThreadId");
// use threadId ...
}
});
p.run();
reg.destroy(p);
}
It’s impressive what Jaroslav built, and I recommend that you take a closer look at this relatively new library.
Being able to read JFR files is great, but you might wonder how to write them ourselves.
Writing JFR Events
Sadly, there is no built-in writer API in Java, but there is the JMC JFR writer API, which is developed by the OpenJDK project, albeit not by the current primary JFR file format maintainers.
To start writing a file, we first create a Recording object using the Recordings class:
Recording recording = Recordings.newRecording(outputStream,
r -> { /* configure settings */ });
The Recording class allows us to register event types and object types, and write events.
In the following, an example that writes two instances of the custom event MyEvent from above, into a file and reads it back using the built-in JFR reader API (GitHub):
import jdk.jfr.consumer.RecordingFile;
import org.openjdk.jmc.flightrecorder.writer.TypesImpl;
import org.openjdk.jmc.flightrecorder.writer.api.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
/**
* Example demonstrating how to write a JFR file using JMC Writer API.
*/
public class JMCWriterExample {
public static void main(String[] args) throws IOException {
Path outputFile = Path.of("example.jfr");
final long startTicks = 1;
try (FileOutputStream fos =
new FileOutputStream(outputFile.toFile())) {
// Initialize a new recording
Recording recording = Recordings.newRecording(fos, r -> {
// Optional: configure recording settings
// Ensure JDK type initialization
r.withJdkTypeInitialization();
// Set start ticks for timestamp consistency in nanoseconds
// 1 seems to work best
r.withStartTicks(startTicks);
});
// Register the event type
Type myEventType = recording.registerType(
"com.example.MyEvent",
"jdk.jfr.Event",
typeBuilder -> {
// Add the implicit startTime field
// the implicit fields must come first
typeBuilder.addField("startTime",
TypesImpl.Builtin.LONG,
field ->
field.addAnnotation(
Types.JDK.ANNOTATION_TIMESTAMP,
"TICKS"));
// Add the custom field
typeBuilder.addField("myfield",
Types.Builtin.STRING);
}
);
// Write an event instance
recording.writeEvent(myEventType.asValue(eventBuilder -> {
// the fields have to be set in order of declaration
eventBuilder.putField("startTime", System.nanoTime() - startTicks);
eventBuilder.putField("myfield", "Hello from JMC!");
}));
// Write another event
recording.writeEvent(myEventType.asValue(eventBuilder -> {
eventBuilder.putField("startTime", System.nanoTime() - startTicks);
eventBuilder.putField("myfield", "Another event");
}));
// Close the recording to finalize the file
recording.close();
}
// Printing file contents for demonstration
RecordingFile.readAllEvents(outputFile)
.forEach(System.out::println);
System.out.println("JFR file written to: " + outputFile);
}
When we run this, we get something like:
com.example.MyEvent {
startTime = 19:08:19.704 (2026-01-14)
myfield = "Hello from JMC!"
}
com.example.MyEvent {
startTime = 19:08:19.704 (2026-01-14)
myfield = "Another event"
}
JFR file written to: example.jfr
The API is rather complicated to use, and the predefined complex types (e.g., stack traces, …) are only approximations of the real types. The RecordingImpl class is a good starting point to see how it can be used.
But what if you only want to modify existing events in an existing file?
Modifying Files with Basic-JFR-Processor
My new basic-jfr-processor library allows you to do precisely this. It’s built on top of the JMC writer API. It supports writing RecordedEvents from the built-in JFR reader API, including support for an event modifier class to allow you to drop events or modify values.
Removing a specific type of event from a file is as simple as:
// Create a modifier that drops events
JFREventModifier modifier = new JFREventModifier() {
@Override
public boolean shouldRemoveEvent(RecordedEvent event) {
return event.getEventType().getName().equals("example.UserLogin");
}
};
// Process the recording
JFRProcessor processor = new JFRProcessor(modifier, inputFile);
try (FileOutputStream out = new FileOutputStream(outputFile.toFile())) {
processor.process(out).close();
}
The JFRProcessor code is also a good example of how to use the JMC writer API to create a file as close as possible to a JFR-generated file. Feel free to reuse the code in your own projects.
Conclusion
The JFR file format is not formally specified; however, there is the possibility of reading and writing JFR files using both built-in and external libraries. This leads to a tool that redacts JFR files, including individual properties. In the last example of the blog post, you saw a basic version of such a tool. In the next blog post, I’ll cover a more complete version.
Thanks for coming along with me and see you next week.
This article is part of my work in the SapMachine team at SAP, making profiling and debugging easier for everyone.




