JEP 515 Ahead-of-Time Method Profiling Explained
-
Last Updated: January 23, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
JEP 515 Ahead-of-Time Method Profiling improves Java startup and warmup performance by reusing method execution profiles from previous runs, allowing the JVM to trigger JIT optimizations earlier without changing application code.
When we say a Java application is “slow at startup,” we usually mix up two different phases: startup and warmup. Understanding this distinction is important before we talk about why JEP 515 exists.
Startup is the time it takes for the JVM to begin executing your application’s main method. During this phase, the JVM initializes itself, loads core classes, sets up the runtime environment, and starts the application thread. A startup is mostly about getting the JVM ready to run.
Warmup, on the other hand, starts after the application is already running. In this phase, the application is executing real logic, but it is not yet running at full speed. This is because most Java code initially runs in interpreted mode. The JVM observes how the application behaves at runtime and gradually decides which parts of the code are important enough to optimize. Only after this observation period does the JIT compiler step in and compile hot methods into highly optimized native code.
This is why Java applications don’t reach peak performance immediately. The JVM cannot safely assume which methods are important just by looking at the source code or bytecode. Java is highly dynamic: classes can be loaded at runtime, methods can be overridden, and execution paths can change based on real input. Because of this, the JVM must run the application first to learn where time is actually being spent. Until enough runtime data is collected, aggressive optimizations are delayed, and performance during warmup is lower.
In traditional long-running server applications, this warmup cost was often acceptable. The application might run for hours or days, so a few seconds or minutes of warmup did not matter much. However, this assumption no longer holds in modern cloud deployments.
Today, many Java applications run as microservices inside containers. These services are frequently restarted due to scaling events, rolling deployments, failures, or autoscaling policies. In serverless and scale-to-zero environments, applications may start and stop repeatedly. In such scenarios, warmup time directly affects user-visible latency, throughput, and even cloud costs. An application that takes too long to warm up may never reach peak performance before it is restarted.
This growing gap between Java’s traditional warmup model and modern deployment patterns is the core problem that JEP 515 aims to address.
Before a Java application reaches peak performance, it goes through a warmup phase during which the JVM observes how the application behaves. This phase exists because Java relies on runtime optimization rather than aggressive compilation upfront. The HotSpot JVM and its Just-In-Time (JIT) compiler work together to identify performance-critical code and optimize it dynamically, based on real execution data collected while the application is running.
The HotSpot JVM is designed around the idea that most applications spend the majority of their time executing a small subset of methods. These frequently executed sections of code are identified as “hot spots.” The JVM continuously monitors execution and feeds this information to the JIT compiler.
The JIT compiler translates selected bytecode methods into optimized native machine code. Because this compilation happens at runtime, the JIT can tailor optimizations to the actual behavior of the application, producing code that is both fast and well-adapted to real workloads.
When the JVM starts, methods are executed in interpreted mode. Interpreted execution allows the application to start quickly, but it is slower because each bytecode instruction is processed step by step by the interpreter.
As the JVM detects that certain methods are executed frequently, those methods are compiled by the JIT into native code. Compiled execution eliminates interpretation overhead and enables low-level optimizations, significantly improving performance. During warmup, interpreted and compiled execution coexist, with the balance gradually shifting toward compiled code for hot methods.
The JIT compiler depends on runtime profiling data to make safe and effective optimization decisions. Java programs are highly dynamic, with late class loading, virtual dispatch, and frequent polymorphism. Without knowing how code behaves at runtime, the JIT cannot confidently apply aggressive optimizations.
By observing real execution patterns, the JVM can optimize based on evidence rather than assumptions. This ensures that optimized code is both fast and correct, and it explains why warmup is a necessary phase in Java’s execution model.
Method execution profiling is the mechanism by which the JVM learns how an application behaves while it runs. Rather than relying on static analysis of source code or bytecode, the JVM gathers concrete data from real executions. This profiling information forms the foundation for JIT compilation and is essential for identifying where optimization effort should be focused.
During execution, the JVM collects profiling data for methods and code paths. This includes how often methods are invoked, how frequently specific bytecode instructions execute, and which branches are usually taken. It also observes object types at call sites and field accesses, which is critical for optimizing virtual method calls.
This data is collected gradually and refined over time, allowing the JVM to build an increasingly accurate picture of method behavior.
Profiling allows the JVM to distinguish between hot and cold methods based on actual usage. Hot methods are those that execute frequently or consume significant CPU time, often appearing in core request-handling paths or tight loops.
Cold methods, in contrast, run rarely and typically do not justify expensive optimization. By identifying this distinction through profiling, the JVM can focus its JIT compilation effort where it delivers the greatest performance benefit.
Static analysis cannot fully predict how a Java application will behave in practice. Java’s dynamic features, such as runtime class loading and polymorphic method calls, mean that execution paths can vary widely depending on input and environment.
Runtime profiling captures real behavior under real conditions. This makes it a far more reliable basis for optimization decisions than static assumptions. The JVM’s reliance on runtime behavior is what enables high peak performance, but it also explains why warmup exists—and why techniques like ahead-of-time method profiling are valuable.
At the heart of Java’s warmup behavior lies a fundamental dependency loop. The JVM needs information about how an application behaves in order to optimize it, but that information only becomes available after the application has already been running for some time. This circular dependency is often described as the chicken-and-egg problem of performance and explains why Java applications cannot be fully optimized from the very beginning of execution.
The JIT compiler cannot aggressively optimize code without first understanding how that code behaves at runtime. It needs profiling data to know which methods are called frequently, which branches are commonly taken, and which object types are encountered at call sites. Until enough of this data is collected, the JVM deliberately postpones high-level optimizations.
This cautious approach avoids wasted work and incorrect assumptions. Optimizing the wrong methods or making speculative assumptions too early can lead to frequent deoptimizations, which are costly. As a result, the JVM prefers to observe execution for a while before committing to expensive compilation and optimization decisions.
There is also a deeper theoretical reason why the JVM cannot rely purely on static analysis to optimize programs. In the JEP, this limitation is explained by referencing Rice’s Theorem, a well-known result from computability theory.
Rice’s Theorem states that it is impossible to determine any non-trivial property of a program’s runtime behavior by analyzing the program alone. Applied to Java, this means the JVM cannot reliably predict which methods will be heavily executed, which branches will dominate, or which execution paths will matter most, simply by inspecting bytecode or source code.
Because Java programs are highly dynamic—supporting runtime class loading, late binding, and polymorphism—actual behavior depends on real inputs and real execution conditions. The only reliable way to discover this behavior is to run the program and observe it. This theoretical limitation explains why runtime profiling is not just an implementation choice in HotSpot, but a necessity.
During the early phase of execution, most methods run in interpreted mode or with minimal compilation. Interpreted execution has higher overhead, and even lightly compiled code lacks the advanced optimizations that the JIT can apply once sufficient profiling data is available.
This is why applications often show lower throughput and higher latency shortly after startup. The JVM is intentionally trading short-term performance for long-term efficiency by gathering the information it needs to produce highly optimized native code later in the application’s lifetime.
In modern deployment environments, applications are often restarted frequently. Microservices may be scaled up and down, containers may be replaced during rolling deployments, and serverless workloads may be created on demand. In these scenarios, applications may not run long enough to fully benefit from JIT optimizations.
Each restart forces the JVM to repeat the same warmup process: interpreting code, collecting profiles, and gradually compiling hot methods. When this happens repeatedly, the application spends a significant portion of its lifetime in a suboptimal performance state. This repeated warmup cost directly impacts response times, resource usage, and overall efficiency, making the traditional warmup model less suitable for short-lived or highly dynamic workloads.
This repeated cost is precisely the problem that JEP 515 addresses by shifting part of the profiling work out of production runs and into earlier training runs.
JEP 515 introduces a simple but powerful idea: instead of collecting method execution profiles during every production run, the JVM can reuse profiles collected during an earlier run of the application. By shifting profiling work out of the critical production startup path, the JVM can begin optimizing code much earlier and significantly reduce warmup time.
A training run is a normal execution of the application whose purpose is to observe and record runtime behavior. During this run, the application executes real code paths, loads real classes, and processes realistic workloads. As the application runs, the HotSpot JVM collects method execution profiles in the same way it always has.
The key difference introduced by JEP 515 is that these profiles are no longer discarded when the JVM exits. Instead, they are preserved so that future runs of the application can start with prior knowledge of which methods are hot and how they behave.
JEP 515 builds on the Ahead-of-Time (AOT) cache introduced earlier. The AOT cache was originally designed to store class-loading and linking information so that the JVM could start faster by avoiding repeated work.
With JEP 515, this cache is extended to also store method execution profiles. These profiles capture information that the JVM would otherwise have to collect again during the warmup phase. When the JVM starts a production run, it loads these cached profiles and makes them immediately available to the JIT compiler. As a result, the JIT can begin compiling and optimizing hot methods much earlier than it otherwise could.
A key design goal of JEP 515 is that it remains completely transparent to applications and frameworks. Developers do not need to modify source code, add annotations, or change application logic. Libraries and frameworks work exactly as they did before.
JEP 515 also does not introduce new execution constraints or rigid assumptions about application behavior. Cached profiles act as a performance hint rather than a fixed rule. The JVM continues to collect profiling data during production runs and can adapt if actual behavior differs from what was observed during training. This ensures that performance improvements come without sacrificing correctness, flexibility, or long-term adaptability.
JEP 515 does not change how the JVM executes applications at a fundamental level. Instead, it changes when certain information becomes available to the runtime. To understand its impact clearly, it helps to look at what happens during a training run and how that information is later used during a production run, while still allowing the JVM to adapt dynamically.
During a training run, the application executes normally under the HotSpot JVM. Methods start in interpreted mode, profiling data is collected as code executes, and the JIT compiler compiles hot methods based on observed behavior. From the application’s point of view, nothing special is happening.
The difference is that, in addition to using this profiling data during the run, the JVM also records selected method execution profiles into the AOT cache. These profiles summarize how methods behaved during execution, such as which methods were frequently executed and how control flow typically proceeded. Once the training run finishes, this profiling information is retained instead of being discarded.
When the application is started again in a production run, the JVM loads the previously stored AOT cache during startup. Along with cached class-loading and linking information, the JVM now also has access to method execution profiles collected earlier.
Because these profiles are available immediately, the JIT compiler does not have to wait for the application to run long enough to rediscover which methods are hot. It can begin compiling and optimizing important methods much earlier in the application’s lifetime. This shortens the warmup period and allows the application to reach high performance sooner after startup.
Crucially, cached profiles do not replace live profiling. During a production run, the JVM continues to observe method execution and collect fresh profiling data just as it always has. If the application’s behavior differs from what was seen during training, the JVM can adjust accordingly.
The JIT compiler can refine optimizations, trigger recompilation, or deoptimize code if earlier assumptions no longer hold. This ensures that cached profiles serve as an initial guide rather than a rigid rule. The end result is a combination of faster startup optimization and continued adaptability, preserving Java’s ability to handle dynamic workloads safely and efficiently.
The primary benefit of JEP 515 is not that Java becomes “faster” in absolute terms, but that it becomes fast much earlier in the application lifecycle. By making method execution profiles available at startup, the JVM reduces the time spent waiting for profiling data before meaningful JIT optimizations can begin. This shortens the warmup phase and allows applications to reach stable, high performance sooner.
Without cached profiles, the JVM must first observe method behavior during execution before it can confidently optimize hot code. This observation phase delays aggressive JIT compilation, which is why applications run slower during early execution.
With JEP 515, method execution profiles collected during a previous run are available immediately when the JVM starts. The JIT compiler can therefore begin optimizing important methods earlier, rather than waiting for them to become hot again. The JVM still performs live profiling, but the initial optimization decisions are guided by real historical data instead of starting from zero.
The result is a noticeably shorter warmup period, especially for applications that repeatedly execute similar workloads across restarts.
The JEP itself provides a concrete example using a small program that relies on the Stream API. Even though the program is short, it loads a large number of JDK classes and causes several methods to become hot.
Here is a simplified version of that example:
package com.javahandson.jep515;
import java.util.*;
import java.util.stream.*;
public class Demo {
static String greeting(int n) {
var words = List.of("Hello", "" + n, "javahandson!");
return words.stream()
.filter(w -> !w.contains("0"))
.collect(Collectors.joining(", "));
}
public static void main(String... args) {
for (int i = 0; i < 100_000; i++) {
greeting(i);
}
System.out.println(greeting(0));
}
}
When this program is run using an AOT cache without method profiles, it completes in about 90 milliseconds. After running a training execution that stores method execution profiles in the AOT cache, the same program completes in about 73 milliseconds, an improvement of roughly 19%. The cached profiles increase the size of the AOT cache by only about 250 KB, which is a small trade-off for the performance gain.
This example demonstrates that even short-lived programs benefit when the JIT compiler can act earlier using cached profiling data.
JEP 515 does not introduce new commands. It uses the existing AOT cache mechanism and simply adds method execution profiles to the cache.
A typical workflow looks like this.
Training run (collect profiles and create the cache):
java -XX:AOTMode=record \
-XX:AOTConfiguration=app.aotconf \
Demo
java -XX:AOTMode=create \
-XX:AOTConfiguration=app.aotconf \
-XX:AOTCache=app.aot \
Demo
Production run (reuse cached profiles):
java -XX:AOTCache=app.aot \
-XX:AOTMode=on \
Demo
During the production run, the JVM loads the cached method profiles at startup, allowing the JIT compiler to optimize hot methods much earlier.
In modern deployments, Java applications often run as microservices inside containers. These services are frequently restarted due to autoscaling, rolling deployments, failures, or scale-to-zero behavior. In such environments, applications may not run long enough for the traditional warmup to complete.
Each restart normally forces the JVM to repeat the same profiling and warmup work. JEP 515 reduces this repeated cost by reusing profiling data across runs. This means services can deliver better performance almost immediately after startup, improving latency, throughput, and resource efficiency.
By shifting profiling work out of production startup paths and into earlier training runs, JEP 515 aligns Java’s runtime behavior more closely with the realities of cloud-native and elastic systems, without sacrificing the adaptability and safety of the JIT compiler.
JEP 515 is not an isolated performance tweak. It is part of a broader, long-term effort to improve Java startup and warmup behavior without giving up the adaptability and safety that the HotSpot JVM provides. To understand its role clearly, it helps to look at how cached profiles differ from full ahead-of-time compilation, why a hybrid approach is preferred, and how this work builds on earlier AOT cache improvements.
Cached method execution profiles and full ahead-of-time (AOT) compilation solve different problems. Cached profiles store knowledge about how code behaves, while AOT compilation stores precompiled native code. With cached profiles, the JVM still relies on the JIT compiler to generate native code at runtime, but it can make better decisions earlier because it already knows which methods are likely to be hot.
Full AOT compilation aims to produce optimized native code before the application ever runs. While this can eliminate warmup entirely in some cases, it requires strong assumptions about application behavior. In a highly dynamic platform like Java, those assumptions may not hold in production, leading to missed optimizations or the need for fallback mechanisms. For this reason, cached profiles are a safer and more flexible approach for many applications.
The HotSpot JVM has always favored adaptability over rigidity. Runtime profiling and JIT compilation allow Java applications to adjust to real workloads, changing inputs, and evolving class hierarchies. However, purely runtime-driven optimization comes with a warmup cost.
A hybrid model combines the strengths of both approaches. Cached profiles and, eventually, cached AOT-compiled code can provide a fast starting point, while the JIT compiler continues to observe and optimize based on live execution. If runtime behavior changes, the JVM can still deoptimize and recompile code safely. This balance allows Java to start fast, warm up quickly, and still reach optimal long-term performance.
JEP 515 builds directly on the AOT cache infrastructure introduced in earlier work. The original AOT cache focused on reducing startup costs by storing class-loading and linking information so that the JVM could avoid repeating this work on every run.
By extending the AOT cache to include method execution profiles, JEP 515 moves optimization-related knowledge into the cache as well. This shifts part of the warmup process out of production runs and into earlier training runs, without changing application behavior or execution semantics.
Taken together, these improvements show a clear direction: Java is evolving toward a runtime that starts faster, warms up sooner, and remains adaptive. JEP 515 is an important step in that direction, laying the groundwork for future performance features that combine ahead-of-time knowledge with just-in-time intelligence.
JEP 515 addresses a long-standing limitation of Java’s performance model: the time it takes for applications to warm up and reach peak performance. Instead of trying to eliminate warmup entirely or replacing the JIT compiler, it takes a pragmatic approach by reusing knowledge gathered from earlier runs. By making method execution profiles available at startup, the JVM can begin optimizing important code much earlier in the application’s lifetime.
What makes this change particularly valuable is that it preserves everything that makes Java robust and adaptable. Applications do not need to be modified, frameworks continue to work as before, and the JVM remains free to adjust its optimizations if runtime behavior changes. Cached profiles act as a helpful starting point, not a rigid rule.
In modern environments where applications are frequently restarted—such as microservices, containers, and autoscaling platforms—this reduction in warmup cost can have a direct and measurable impact on performance and efficiency. JEP 515 fits naturally into Java’s evolving performance roadmap, combining ahead-of-time knowledge with just-in-time intelligence to deliver faster startup without sacrificing long-term optimization.