JFR Cooperative Sampling in Java 25 (JEP 518)

  • Last Updated: January 23, 2026
  • By: javahandson
  • Series
img

JFR Cooperative Sampling in Java 25 (JEP 518)

JFR Cooperative Sampling in Java 25, introduced by JEP 518, redesigns how JDK Flight Recorder samples thread stacks. By deferring stack walking to safepoints, it improves JVM stability, reduces profiling risk, and minimizes safepoint bias.

1. Introduction: What JEP 518 Brings to JFR

JEP 518 is an internal improvement to Java Flight Recorder (JFR) in Java 25 that makes profiling more stable and safer. It does not add new commands or APIs for developers. Instead, it changes how JFR collects execution-time profiling data, especially stack traces, so that profiling itself does not become a source of JVM problems.

Profiling works by observing where a running program spends time. JFR does this by periodically sampling the call stacks of Java threads and later summarising those samples into reports using tools like JFR and JDK Mission Control. For this to work correctly, JFR must pause a thread and walk its stack to understand which methods are executing.

Before JEP 518, JFR sampled stacks asynchronously. It could stop a thread at almost any point and attempt to read its stack immediately. This helped avoid safepoint bias—the risk of missing frequently executed code—, but it came with a cost. Stack metadata inside the JVM is only guaranteed to be valid at specific locations called safepoints. Sampling outside these points required complex heuristics, and when those heuristics failed, they could produce incorrect stack traces or even crash the JVM in rare cases.

JEP 518 introduces cooperative sampling to solve this problem. Instead of walking the stack immediately, JFR now records a lightweight sampling request and allows the thread to continue running. When the thread naturally reaches its next safepoint, the JVM safely reconstructs the stack trace at that point. The thread effectively cooperates with the profiler by completing the sampling work at a safe moment.

This change is important for profiling stability in Java 25. Modern Java applications run with high concurrency, frequent class unloading, and heavy JVM optimisation. In such conditions, unsafe stack parsing becomes increasingly risky. By ensuring that stack traces are only created at safepoints, JEP 518 makes JFR profiling more reliable, more predictable, and better suited for continuous use in production environments.

In simple terms, JEP 518 makes JFR safer without changing how developers use it. You still get accurate execution-time profiles, but with far less risk that profiling itself will disturb or destabilise your application.

2. How JDK Flight Recorder Does Execution-Time Profiling

JDK Flight Recorder (JFR) finds performance problems by observing where a Java program spends time while it is running. It does not watch every method call, because that would slow the application down. Instead, it uses a lightweight technique called stack sampling, which gives a good performance picture with very low overhead.

2.1. Stack sampling and wall-clock profiling

JFR periodically takes a snapshot of what each Java thread is doing. At regular intervals (for example, every few milliseconds), it pauses a thread briefly and records its call stack—the list of methods that are currently executing. Each snapshot represents what the program was doing at that exact moment in real, elapsed time.

Because these snapshots are taken over normal running time, JFR measures wall-clock time. This means it shows delays caused not only by CPU usage, but also by waiting, locking, sleeping, or I/O. If a method appears often in these samples, it means the program spends a lot of real time in or under that method.

2.2. How JFR builds a profile from samples

Every captured stack snapshot is stored as a JFR event. As the program continues to run, JFR collects many such events. Later, tools like JFR or JDK Mission Control read these events and combine them into a performance report.

The idea is simple: the more frequently a method appears in the samples, the more time the program spends there. For example, if a method shows up in 30% of the samples, it likely accounts for about 30% of the program’s execution time. This statistical approach is accurate enough for performance tuning and avoids the high cost of detailed tracing.

In short, JFR builds execution-time profiles by taking many small, quick snapshots of running threads and summarising them. This is the foundation on which JEP 518 improves stability and safety.

3. The Real Problem with Asynchronous Stack Sampling

To understand why JEP 518 was needed, it helps to look at how JFR used to sample thread stacks and where that approach caused real problems. The earlier design focused on accuracy, but it came with risks that affected JVM stability.

3.1. Sampling outside safepoints

In the JVM, a safepoint is a well-defined place in the code where the JVM knows that a thread’s state is fully consistent. Important internal metadata, such as information needed to interpret stack frames, is guaranteed to be valid only at these points.

Older versions of JFR sampled thread stacks asynchronously, meaning a thread could be stopped at almost any instruction, not necessarily at a safepoint. This was done deliberately to avoid safepoint bias, where frequently executed code might never be sampled if it rarely reaches safepoints. While this improved profiling accuracy, it meant JFR often tried to inspect thread stacks when the JVM was not in a fully safe state to do so.

3.2. Heuristic-based stack walking

Because stack metadata is not always reliable outside safepoints, JFR had to rely on heuristics to walk a thread’s stack. These heuristics attempted to guess how to interpret stack frames even when the JVM could not guarantee correctness.

Heuristic-based stack walking is inherently fragile. Most of the time it works, but under certain conditions—such as aggressive JIT optimisations, concurrent class unloading, or transitions between Java and native code—the guesses can be wrong. When that happens, the stack trace may be incomplete, incorrect, or inconsistent.

3.3. JVM stability and crash risks

The biggest issue was not just inaccurate profiling data, but JVM stability. Incorrect stack parsing can lead to memory access errors inside the JVM itself. Although JFR included platform-specific crash-protection mechanisms to reduce this risk, those protections were not foolproof. In particular, concurrent activities like class unloading could still cause failures.

This meant that, in rare but serious cases, enabling JFR profiling could crash the JVM. For a tool intended to run continuously in production, this risk was unacceptable.

In summary, asynchronous stack sampling traded safety for accuracy. While it reduced safepoint bias, it introduced fragile stack-walking logic and real crash risks. JEP 518 addresses these problems by redesigning how samples are collected, making stability the top priority without abandoning profiling accuracy.

4. Safepoints and the Safepoint Bias Problem

To understand the core idea behind JEP 518, it is important to first understand safepoints and why they matter so much for profiling inside the JVM. This concept sits at the heart of the problem JEP 518 is trying to solve.

4.1. What is a safepoint?

A safepoint is a specific location in a Java thread’s execution where the JVM knows that the thread is in a fully consistent and well-defined state. At a safepoint, the JVM can safely perform operations such as garbage collection, class unloading, or stack inspection. Threads reach safepoints naturally at certain points in the code, such as method calls, loop back-edges, or other places chosen by the JVM.

The key idea is that safepoints are moments where the JVM has complete and correct knowledge about a thread’s execution state.

4.2. Why stack metadata is reliable only at safepoints

To build a stack trace, the JVM relies on internal metadata that describes how methods are laid out on the stack. This metadata is guaranteed to be correct only when a thread is stopped at a safepoint. Outside safepoints, a thread may be in the middle of compiled code execution, optimised instructions, or transitions between Java and native code. In those moments, the JVM cannot safely or reliably interpret the stack.

This is why walking a stack outside safepoints is dangerous. The JVM may not know how to correctly map memory to Java method frames, which can lead to incorrect stack traces or internal errors.

4.3. What safepoint bias means in profiling

If a profiler only samples thread stacks at safepoints, another problem appears: safepoint bias. Not all code reaches safepoints equally often. Some methods, especially tight loops or heavily optimised code, may run for long periods without hitting a safepoint.

As a result, such code can be underrepresented or completely missing in profiling data. This creates a biased profile that overemphasises code near safepoints and underrepresents code that actually consumes significant execution time. The profile may look clean and safe, but it can be misleading.

This trade-off between safety and accuracy is the central challenge in execution-time profiling. JEP 518 addresses this challenge by combining safepoint safety with a cooperative approach that reduces bias, forming the conceptual foundation of the new JFR sampling design.

5. How JEP 518 Separates Sampling from Stack Walking?

A key idea in JEP 518 is that sampling is no longer a single action. Instead, it is split into two clearly separated phases. Understanding this two-phase model is essential to see how JEP 518 improves stability without reintroducing heavy safepoint bias.

The first phase is requesting a sample, and this phase is time-based. At fixed intervals, JFR decides that it is time to take a sample. The sampler thread briefly suspends the target thread, records only minimal execution state, such as the program counter and stack pointer, and places a sample request into an internal queue associated with that thread. The target thread is then immediately resumed. No stack walking happens at this stage. Importantly, this decision to sample is not tied to safepoints. It can occur while the thread is executing any Java code, which is why this approach does not limit sampling to safepoint locations.

The second phase is stack reconstruction, and this phase is safepoint-based. After the sample request is recorded, the target thread continues running normally. When it eventually reaches its next safepoint, the JVM checks whether there are any pending sample requests for that thread. If there are, the JVM safely reconstructs the stack trace at the safepoint and emits a JFR execution-time profiling event. Because this work happens at a safepoint, the JVM has reliable stack metadata and can build the stack trace without heuristics or risk.

This separation is the key insight of JEP 518. The timing of sampling remains accurate, because sample requests are made independently of safepoints. At the same time, stack walking becomes safe because it happens only when the JVM is in a well-defined state. Safepoint bias is reduced compared to safepoint-only sampling, while unsafe asynchronous stack parsing is completely avoided.

With this two-phase model in mind, the design of cooperative sampling—and its benefits—becomes much easier to understand.

6. JEP 518 Solution: Cooperative Sampling Explained

JEP 518 solves the problems of unsafe stack sampling by changing who does the work and when it is done. Instead of forcing stack analysis at risky moments, JFR now lets the running thread participate in profiling in a controlled and safe way. This design is called cooperative sampling, and it is the core technical change introduced by this JEP.

6.1. Lightweight sample request by the sampler thread

When it is time to take a profiling sample, JFR still uses a dedicated sampler thread. However, the sampler thread no longer tries to walk the target thread’s stack immediately. Doing so outside safepoints was the main source of instability earlier.

Instead, the sampler thread performs only a very small and safe action. It briefly suspends the target thread, records minimal information such as the program counter and stack pointer, and creates a sample request. This request is added to a small internal queue associated with the target thread. After that, the target thread is resumed almost immediately.

At this stage, no stack walking happens at all. The sampler thread avoids complex logic, avoids heuristics, and avoids touching fragile stack metadata. This makes the sampling request extremely cheap and safe, and it can even be triggered from contexts like signal handlers or hardware events.

6.2. Stack reconstruction at the next safepoint

Once the sample request is created, the target thread continues executing normally. Nothing special happens until the thread reaches its next safepoint during regular execution.

When the thread arrives at a safepoint, the JVM’s safepoint handling code checks whether there are any pending sample requests in the thread’s queue. If it finds one, the thread itself performs the stack reconstruction at that moment.

Because this happens at a safepoint, the JVM has complete and reliable stack metadata. The stack trace can be built safely and correctly, without guessing or heuristics. After reconstructing the stack, JFR emits an execution-time profiling event, just like before.

An important detail is that this stack reconstruction now runs on the target thread, not on the sampler thread. This simplifies the implementation significantly. For example, memory allocation is allowed, and the code is no longer constrained by the restrictions of running inside an asynchronous sampling context.

6.3. How safepoint bias is reduced without unsafe parsing

At first glance, sampling only at safepoints might seem like a step backward because of safepoint bias. JEP 518 addresses this by separating when a sample is requested from when the stack is actually parsed.

The sampling decision still happens at regular time intervals, independent of safepoints. This preserves the statistical nature of profiling. The thread is marked for sampling at the correct time, even if it does not immediately reach a safepoint. The actual stack trace is then reconstructed at the next safepoint, with adjustments to account for the delay.

This approach avoids unsafe stack parsing while still keeping the profile representative of real execution time. The JVM compensates for the fact that the safepoint might occur slightly later, reducing the distortion that pure safepoint-only sampling would introduce.

Why this design is a major improvement?

Cooperative sampling brings several important benefits together:

  • Stack walking happens only at safepoints, where it is guaranteed to be safe.
  • The sampler thread becomes simpler and more scalable.
  • Risky heuristics are removed from the critical path.
  • Profiling remains low overhead and suitable for production use.

Most importantly, this design aligns profiling accuracy with JVM correctness. JEP 518 does not choose between safety and usefulness—it combines both. This is why it is such a significant improvement to JFR’s execution-time profiling in Java 25.

7. Why Cooperative Sampling Is Better

Cooperative sampling is a clear improvement over the earlier asynchronous stack-sampling approach because it fixes real, practical problems that affected JVM stability and profiling reliability. This change is not about adding more data, but about collecting the same kind of data in a much safer and more scalable way.

7.1. Improved JVM stability and safety

The most important benefit of cooperative sampling is improved JVM safety. With JEP 518, stack traces are reconstructed only at safepoints, where the JVM guarantees that stack metadata is valid and consistent. This completely avoids the need to parse stacks at arbitrary execution points, which previously required fragile heuristics.

Earlier, when stack parsing went wrong—especially during concurrent activity like class unloading—it could lead to incorrect stack traces or, in rare cases, JVM crashes. Cooperative sampling removes this risk from the normal profiling path. As a result, JFR becomes reliable enough to run continuously, even in production systems, without profiling itself, threatening JVM stability.

7.2. Lower profiling overhead

Cooperative sampling also reduces the overhead of profiling. The sampler thread now performs only minimal work: it records a small amount of state and queues a sample request. It does not walk stacks, allocate memory, or run complex logic.

The heavier work of reconstructing the stack and emitting the profiling event happens later, on the target thread, at a safepoint. This spreads the cost naturally and avoids putting pressure on a single sampler thread. Because of this design, execution-time profiling remains lightweight and suitable for long-running applications.

7.3. Better scalability and simpler implementation

From an implementation perspective, cooperative sampling is both simpler and more scalable. Removing heuristic-based stack walking eliminates large amounts of complex, platform-specific code. Stack reconstruction now runs in a normal JVM context, making the code easier to reason about, maintain, and evolve.

Scalability improves as well. Since the sampler thread does very little work per sample, profiling overhead grows much more slowly as the number of threads increases. This is especially important for modern Java applications that rely on high concurrency.

In summary, cooperative sampling improves JFR where it matters most: it makes profiling safer for the JVM, cheaper to run, and more scalable for modern workloads. These benefits are why JEP 518 is such an important improvement in Java 25.

8. Limitations and What Comes Next

While JEP 518 significantly improves the safety and stability of execution-time profiling in JFR, it does not eliminate all limitations. The JEP is explicit about what it does and does not solve, and understanding these boundaries helps set the right expectations.

8.1. Native code sampling limitations

The cooperative sampling approach introduced by JEP 518 works well when a thread is executing Java code, whether interpreted or JIT-compiled. However, when a thread is running native code, the situation is different. In native execution, the JVM cannot always reconstruct a full Java stack at a safepoint, because parts of the execution may not map cleanly back to Java frames.

In such cases, JFR continues to rely on the existing asynchronous sampling mechanism. This means that native code paths do not fully benefit from the new cooperative model. While this does not introduce new risks beyond what already existed, it also means that JEP 518 does not completely replace the older approach in all scenarios.

8.2. Remaining sources of safepoint bias

JEP 518 reduces safepoint bias, but it does not eliminate it entirely. The JEP itself acknowledges situations where a stack trace cannot be fully reconstructed, even at a safepoint. One example is when execution is inside methods that the JVM has replaced with intrinsic implementations.

In such cases, the recorded stack trace may stop at the last available Java frame. This introduces some bias, because the actual execution may be happening deeper in code that is not fully visible to the JVM’s stack-walking logic. Addressing these cases is identified as future work, not something solved by JEP 518.

8.3. Relationship with JEP 509 (CPU-Time Profiling)

JEP 518 also plays an important foundational role for other profiling improvements in Java 25. In particular, JEP 509 (CPU-Time Profiling) builds on the cooperative sampling mechanism introduced here.

By making stack sampling safer and more reliable, JEP 518 provides the infrastructure needed for accurate CPU-time measurements. Without stable stack reconstruction at safepoints, CPU-time profiling would inherit the same risks and limitations as earlier execution-time sampling. In this sense, JEP 518 is not just an isolated improvement, but a necessary step toward more advanced and reliable profiling capabilities in the JDK.

Overall, this JEP keeps JFR moving in a safer and more robust direction. It improves what matters today, while clearly outlining what remains to be addressed in future JVM work.

9. Difference Between JEP 518 and JEP 509

Although JEP 518 and JEP 509 are part of the same Java 25 release, they solve different problems and operate at different layers of Java Flight Recorder.

JEP 518 focuses on how JFR safely collects stack samples. It redesigns the internal sampling mechanism to avoid unsafe stack parsing by separating sampling from stack walking. Its goal is stability and correctness. It does not introduce new metrics or views for developers. Instead, it makes existing execution-time profiling safer and more reliable.

JEP 509, on the other hand, focuses on what is being measured. It introduces CPU-time profiling, which shows how much actual CPU time a thread or method consumes, rather than wall-clock time. This is especially useful for understanding CPU-bound workloads, parallel execution, and performance under contention.

In simple terms:

JEP 518 is about “how profiling is done safely.”

JEP 509 is about “what kind of time is being measured.”

JEP 509 relies on the mechanism introduced by JEP 518. Without safe and stable stack sampling, CPU-time profiling would inherit the same risks that earlier asynchronous sampling had. This is why JEP 518 is considered a foundational change, while JEP 509 builds on top of it.

Note* JEP 518 improves the safety of stack sampling, while JEP 509 uses that safer foundation to measure CPU time accurately.

10. Conclusion

JEP 518 is a quiet but important improvement to Java Flight Recorder in Java 25. It does not change how developers enable or use JFR, but it fundamentally improves how execution-time profiling works under the hood. Addressing long-standing risks in asynchronous stack sampling, it makes profiling safer and more reliable for modern Java applications.

The key idea behind this JEP is the clear separation of concerns. Sampling decisions remain time-based, so profiling accuracy is preserved. At the same time, stack reconstruction is deferred to safepoints, where the JVM’s internal state is stable and well understood. This cooperative approach removes the need for fragile heuristics and significantly reduces the risk of JVM crashes during profiling.

JEP 518 does not claim to solve everything. Sampling native code and certain intrinsic paths still have limitations, and some sources of safepoint bias remain. Importantly, these limits are acknowledged openly in the JEP, keeping expectations realistic and the design honest.

Perhaps most importantly, JEP 518 lays the groundwork for future profiling improvements. Its mechanism is directly leveraged by JEP 509 for CPU-time profiling, showing that this change is part of a broader effort to make JFR a stronger, safer, and more production-ready profiling tool.

In summary, JEP 518 strengthens JFR where it matters most: stability, safety, and long-term reliability. It is a foundational improvement that makes always-on profiling in Java 25 more trustworthy, without sacrificing the insights developers depend on.

Leave a Comment