Java Flight Recorder Tutorial: How to Profile Java Applications

 

Transcript:

Java Flight Recorder is a profiling and diagnostics tool built into the OpenJDK. Let’s look at how we can use it with Java applications, both locally and in containers, and how to analyze the recordings in Java Mission Control. We will go through both theory and practice, so if you are already familiar with JFR, you can jump directly to the practical section.

JFR, or Java Flight Recorder, is a powerful tool for troubleshooting Java application performance at runtime. Think of it as a flight recorder for your JVM. It collects runtime events with timestamps and stores them in binary log files. These recordings can be analyzed in Java Mission Control, an open-source graphical tool for diagnostics.

JFR can record CPU usage, garbage collection activity, thread behavior, memory allocation, I/O, and many other runtime aspects. It supports hundreds of event types out of the box, and you can also define custom events.

In simple terms, JFR helps answer questions like what the JVM is doing, where time is spent, whether threads are blocked or overloaded with allocations, and whether garbage collection or CPU usage shows abnormal behavior.

If you are just starting with JFR, here are a few key points to understand. It is built into OpenJDK and does not require separate installation. It has very low overhead, making it suitable for production troubleshooting. You can start recordings at JVM startup or attach later using the jcmd tool. JFR also supports continuous recordings and includes command-line tools such as jfr summary and jfr view.

However, JFR is not a universal profiling tool. There are cases where it is not the best fit.

For deep native profiling, JFR is limited because it focuses on JVM events rather than kernel-level or native library behavior. In such cases, tools like perf or native profilers are more appropriate.

For distributed systems, JFR is not ideal because it captures only a single JVM’s event stream and cannot reconstruct full request flows across services, queues, and databases. Distributed tracing tools are better suited for that.

For database profiling, JFR can show that threads are waiting on I/O, but it cannot precisely explain why a query is slow. For that, you need database-level tools or application-level instrumentation.

Now let’s look at how to use JFR.

You can start a recording at application startup using JVM flags, including options for duration and output file name. Additional flags such as unlock diagnostic VM options and debug non-safe points can improve profiling accuracy by enabling more detailed event capture.

Alternatively, you can use jcmd, a built-in JDK tool, to start or stop recordings at runtime. This allows you to profile a running application at any point.

JFR also supports continuous recording mode. In this mode, you can limit event types to reduce overhead and configure file rotation. In JDK 25, the default maximum size for continuous recordings is 250 MB, but this can be adjusted. You can also combine maximum size with maximum age to control retention. For example, setting a maximum age of one hour keeps only the last hour of data. Additionally, setting dump on exit to true ensures the recording is saved when the JVM shuts down.

JFR can also be used inside containers.

With Dockerfiles, you can start a JFR recording at container startup. In a typical setup, you build the application in one stage using a hardened Liberica JDK image based on Alpine Linux, then run it in a minimal JRE-based runtime image. A hardened image reduces the attack surface and improves security.

In the entry point, you pass JFR options, and the recording starts automatically when the container launches.

If you use Buildpacks, enabling JFR is even simpler. Setting BP_JFR_ENABLED to true automatically starts recording. By default, the recording is written to recording.jfr in the /tmp directory when the JVM exits. You can customize this using BP_APPEND_JAVA_TOOL_OPTIONS to control duration and file name. The file can later be copied from the container using docker cp.

To profile a running container, you can use jcmd. However, since jcmd is part of the JDK and not the JRE, it is not typically included in production images. Production containers are often minimal or distroless and may not even include a shell.

To solve this, you can use ephemeral containers. These are temporary containers started for debugging or profiling purposes. They run alongside the production container for a short time.

In this approach, you deploy the application without JFR enabled, then start an ephemeral container with the JDK. This container shares the same process namespace as the application. You use jcmd to find the JVM process ID and then start a JFR recording. After profiling is complete, you extract the recording using docker cp.

Now let’s move to JFR analysis.

When you open a JFR file in Java Mission Control, it first shows automated analysis results with rule-based insights. These highlight potential issues such as memory pressure, GC activity, thread contention, or excessive exceptions.

This view is useful as a starting point, but it should not be treated as a final diagnosis. It simply guides you toward areas that deserve deeper inspection.

In our example, the recording shows heavy activity during Spring Boot startup, including memory usage, socket reads, and many exceptions. At first glance, this might look alarming, but context is important.

The garbage collection analysis shows frequent but very short pauses, typically just a few milliseconds. This indicates that GC is active but not a major bottleneck.

Memory analysis shows that most allocations are in byte arrays, followed by strings, integers, and class loading-related objects. This is typical during application startup due to framework initialization, class loading, and buffering.

Thread analysis helps determine whether the JVM is actively processing or mostly waiting. Zooming into thread timelines provides deeper insight into execution states.

Method profiling shows where execution time is spent at the method level. In this case, bcrypt encipher appears near the top, indicating password hashing activity. This is expected in authentication or security initialization scenarios.

The exceptions view shows a high number of exceptions. However, many of them are related to class loading, reflection, and framework internals rather than actual business logic failures. This is common during startup phases.

JFR also captures JVM and hardware configuration details, which provide important context for analyzing recordings or sharing them with performance engineers.

Overall, this example shows that JFR is highly context dependent. What initially looks like a problem may simply be normal startup behavior in a Spring Boot application, including class loading, dependency initialization, memory allocation, and I/O activity.

JFR provides the data needed to move from assumptions to evidence and understand what the JVM is truly doing at runtime.

Summary

Java Flight Recorder (JFR) is a built-in OpenJDK tool for collecting detailed runtime data about JVM behavior with very low overhead, making it suitable even for production use. It records events such as CPU usage, garbage collection, thread activity, memory allocation, and I/O, which can be analyzed in Java Mission Control. The video demonstrates how to start recordings locally, manage them with JVM flags or jcmd, and run JFR inside containers, including advanced use with ephemeral containers for live systems. It also shows how to interpret real Spring Boot recordings by analyzing GC behavior, memory allocation, thread states, method profiling, and exceptions in context. Overall, JFR helps move from surface-level symptoms to real root causes of performance issues in Java applications.

About Catherine

Java developer passionate about Spring Boot. Writer. Developer Advocate at BellSoft

Social Media

Videos
card image
Mar 26, 2026
Java Developer Roadmap 2026: From Basics to Production

Most Java roadmaps teach tools. This one teaches order — the only thing that actually gets you to production. You don’t need to learn everything. You need to learn the right things, in the right sequence. In this video, we break down a practical Java developer roadmap for 2026 — from syntax and OOP to Spring, databases, testing, and deployment. Structured into 8 levels, it shows how real engineers grow from fundamentals to production-ready systems. We cover what to learn and what to ignore: core Java, collections, streams, build tools, Git, SQL and JDBC before Hibernate, the Spring ecosystem, testing with JUnit, and deployment with Docker and CI/CD. You’ll also understand why most developers get stuck — jumping into frameworks too early, skipping SQL, or treating tools as knowledge. This roadmap gives you a clear path into real-world Java development — with priorities, trade-offs, and production context.

Videos
card image
Mar 19, 2026
TOP-5 Lightweight Linux Distributions for Containers

In this video, we compare five lightweight Linux distributions commonly used as base images: Alpine, Alpaquita, Chiseled Ubuntu, RHEL UBI Micro, and Wolfi. There are no rankings or recommendations — just a structured look at how these distros differ so you can evaluate them in your own context.

Further watching

Videos
card image
Apr 22, 2026
Dynamic SQL Queries with Spring Data JPA in 6 Minutes

If your repository layer has multiple queries for different filter combinations, your data access logic is already getting harder to maintain. In this video, we implement dynamic SQL queries in Spring Data JPA using Specifications — a composable approach that helps avoid query duplication and keeps your filtering logic clean. We build a flexible filtering system with optional parameters (category, language, format, price) and show how Specification.unrestricted() skips empty filters, while Specification.allOf(...) combines them into a single query. We also address a common issue: string-based field access. It’s fragile and can break at runtime when your model changes. Using the JPA Static Metamodel, we move to compile-time safety. The result is a cleaner, more maintainable way to implement dynamic filtering in Spring-based applications.

Videos
card image
Apr 8, 2026
Best Oracle Java Alternatives in 2026 Comparison of OpenJDK Distributions

A comparison of major OpenJDK distributions (Temurin, Liberica, Zulu, Corretto, Semeru, etc.), covering who maintains them, how updates are delivered, and what lifecycle guarantees they provide. We also explain why upstream OpenJDK isn’t production-ready and how your vendor choice impacts real-world systems. Useful for Spring Boot, containers, and Kubernetes to avoid hidden risks and choose the right runtime.

Videos
card image
Apr 2, 2026
Java Memory Options You Need in Production

JVM memory tuning can be tricky. Teams increase -Xmx and assume the problem is solved. Then the app still hits OOM. Because maximum heap size is not the only thing that affects memory footprint. The JVM uses RAM for much more than heap: metaspace, thread stacks, JIT/code cache, direct buffers, and native allocations. That’s why your process can run out of memory while heap still looks “fine”. In this video, we break down how JVM memory actually works and how to control it with a minimal, production-safe set of flags. We cover heap sizing (-Xms, -Xmx), dynamic resizing, direct memory (-XX:MaxDirectMemorySize), and total RAM limits (-XX:MaxRAMPercentage) — especially in containerized environments like Docker and Kubernetes. We also explain GC choices such as G1, ZGC, and Shenandoah, when defaults are enough, and why GC logging (-Xlog:gc*) is mandatory before tuning. Finally, we show how to diagnose failures with heap dumps and OOM hooks. This is not about adding more flags. It’s about understanding what actually consumes memory — and making decisions you can justify in production.