Java Ahead-of-Time (AOT) Class Loading And Linking Example
Ahead-of-Time (AOT) Class Loading & Linking is an optimization technique introduced in modern Java runtimes to reduce application startup time and improve runtime performance. Traditionally, the JVM performs class loading, linking, and initialization lazily during execution. AOT shifts part of this work to an earlier phase, enabling faster startup and predictable performance. With advancements in JDK 24 and beyond, new mechanisms like AOT cache creation and profiling have made this approach more practical and efficient. Let us delve into understanding Java AOT class loading, linking, and how it enhances application startup and performance.
1. Foundations of Ahead-of-Time (AOT)
Ahead-of-Time (AOT) compilation and class loading aim to shift work traditionally done at runtime (class loading, verification, and linking) to an earlier phase. This reduces JVM startup overhead and improves application responsiveness, which is especially critical in microservices, serverless, and containerized deployments. In a typical JVM execution, classes are loaded lazily and linked on demand. AOT changes this model by preparing and storing this information in advance, allowing the JVM to skip repetitive work during startup.
1.1 AOT Cache Mechanism in JDK 24
In JDK 24, the AOT cache mechanism allows the JVM to persist class metadata after an initial run. This cache contains preprocessed information that avoids recomputing expensive operations during subsequent executions. The cache includes resolved constant pool entries (such as method and field references), pre-linked method handles and invokedynamic call sites, class hierarchy and dependency graphs, and verification results that allow the JVM to skip bytecode verification in subsequent runs. As a result, classes are loaded, verified, and linked ahead of execution, symbol resolution is performed once and reused across runs, and overall classloader overhead is reduced, leading to improved cold start performance. However, this model required a warm-up execution. The application had to run at least once to populate the cache, making it less ideal for ephemeral environments like short-lived containers or serverless functions.
1.2 One-Shot AOT Cache Generation (JEP 514)
JEP 514 introduces a significant improvement by enabling One-Shot AOT Cache Creation, where the cache is generated in a single controlled execution. This eliminates the need for repeated warm-up cycles and allows developers to generate the cache during build or deployment phases. As a result, there is no need for multiple application runs to build an effective cache; it seamlessly integrates into CI/CD pipelines (such as during Docker image builds), and it improves developer productivity while reducing overall operational complexity.
The typical workflow involves running the application in a controlled environment during the build stage, generating the AOT cache file, packaging the cache along with the application artifact, and then reusing this cache across all deployments to ensure faster startup and consistent performance. This makes AOT highly suitable for cloud-native architectures where fast startup and reproducibility are critical.
1.3 Limitations of the One-Shot Approach
Despite its simplicity, the one-shot approach has inherent trade-offs due to the lack of runtime intelligence.
- May not capture real-world traffic patterns or production workloads.
- Misses hot paths that only emerge under sustained load.
- Limited optimization opportunities compared to adaptive JIT compilation.
- Risk of over-optimizing rarely used code paths.
Because the cache is generated from a single execution, it may not reflect actual usage patterns, leading to suboptimal performance in dynamic or highly variable workloads.
1.4 Enhancing AOT with Method Profiling (JEP 515)
JEP 515 enhances the AOT model by incorporating method-level profiling data into the cache. This allows the JVM to make smarter decisions based on observed runtime behavior.
- Identifies hot methods (frequently executed code paths).
- Captures invocation counts and execution frequency.
- Enables better inlining and optimization decisions.
- Improves branch prediction and reduces execution latency.
By combining AOT with profiling data, the JVM can approximate some of the benefits of Just-In-Time (JIT) compilation while still maintaining fast startup times. This effectively creates a hybrid model:
- AOT provides fast startup and precomputed metadata.
- Profiling brings runtime awareness and smarter optimizations.
1.5 Runtime vs. Cached Profiling: A Comparison
Understanding the difference between runtime profiling and cached profiling is key to evaluating AOT effectiveness.
| Aspect | Runtime Profiling | Cached Profiling |
|---|---|---|
| Execution | Occurs dynamically during application runtime | Captured earlier and stored in cache |
| Startup Time | Slower due to on-the-fly analysis | Faster due to precomputed data |
| Optimization Quality | Highly accurate and adaptive | Depends on the representativeness of captured data |
| Flexibility | Adapts to changing workloads | Static unless the cache is regenerated |
| Best Use Case | Long-running applications | Microservices, serverless, fast-start apps |
In practice, modern JVM strategies increasingly combine both approaches—leveraging AOT for startup performance and runtime profiling for long-term optimization.
2. Code Example
The following example is used to demonstrate a simple computation and measure execution time. However, to properly understand the benefit of Ahead-of-Time (AOT) class loading and linking, we must compare two execution modes:
- On-Demand Class Loading & Linking (Baseline JVM behavior) — classes are loaded, verified, and linked during execution.
- AOT Cached Execution — class metadata, resolution, and linking information are precomputed and reused.
public class AOTExample {
public static void main(String[] args) {
long start = System.nanoTime();
int result = computeSum(1000000);
long end = System.nanoTime();
System.out.println("Result: " + result);
System.out.println("Execution Time: " + (end - start) + " ns");
}
public static int computeSum(int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
return sum;
}
}
2.1 Code Explanation
This Java program demonstrates a simple computation along with execution time measurement. The main method starts by capturing the current time in nanoseconds, then calls the computeSum method with an input of 1,000,000 to calculate the sum of integers from 0 up to (but not including) that number using a loop. The result is stored, and the end time is recorded immediately after execution. The program then prints both the computed result and the total execution time (difference between end and start time) in nanoseconds. The computeSum method itself iterates from 0 to n-1, accumulating the sum in a variable and returning the final value, making this example useful for understanding basic performance measurement and how such computations might benefit from optimizations like Ahead-of-Time (AOT) caching.
2.2 Code Output
Result: 1783293664 Execution Time: 1200000 ns
The output shows two key results from the program execution. The value Result: 1783293664 represents the computed sum of integers from 0 to 999,999; however, this value is incorrect due to integer overflow since the expected sum exceeds the maximum limit of a 32-bit int in Java (2,147,483,647), causing the result to wrap around into a negative or reduced positive value. The Execution Time: 1200000 ns indicates that the computation took approximately 1.2 milliseconds (1,200,000 nanoseconds) to complete, reflecting the time taken by the loop execution and method call, and this measurement can vary depending on system performance, JVM optimizations, and whether techniques like AOT or JIT compilation are in effect.
Note: Execution time will vary depending on system and JVM optimizations.
2.3 Execution Comparison (On-Demand vs AOT)
To properly observe the impact of Ahead-of-Time (AOT) class loading and linking, you must run the same program in two distinct modes:
- Mode 1: Standard JVM execution (On-Demand class loading)
- Mode 2: AOT Cached execution (using precomputed class metadata)
2.3.1 Running in On-Demand Mode (Baseline)
In the default JVM execution mode, class loading and linking occur at runtime: first compile the program using javac AOTExample.java, then run it with java AOTExample, and observe output such as Result: 1783293664 and an execution time of around 2–3 ms (cold start); during this process, the JVM performs class loading, bytecode verification, symbol resolution and linking, followed by execution.
2.3.2 Running in AOT Mode (JDK 24+)
AOT mode requires two phases:
- Generate the AOT cache
- Run using the generated cache
2.3.2.1 Generate AOT Cache (One-Shot)
Run the application once in a controlled mode to create the cache:
java -XX:AOTMode=record -XX:AOTConfiguration=./aot-config.json -XX:AOTCache=./aot-cache AOTExample
This step executes the program while simultaneously recording class loading, linking, and resolution data, and then stores this preprocessed information in the AOT cache for reuse in subsequent runs.
2.3.2.2 Run Using AOT Cache
Now execute the same program using the precomputed cache:
java -XX:AOTMode=replay -XX:AOTCache=./aot-cache AOTExample
This step runs the program using the precomputed AOT cache, skipping repeated class loading and linking work, reusing preprocessed metadata, and thereby reducing overall startup overhead.
3. Conclusion
Ahead-of-Time Class Loading & Linking is a significant step toward improving Java application startup and performance. While JDK 24 introduced foundational AOT caching, JEP 514 and JEP 515 refine the approach with one-shot cache creation and method profiling.
The future of Java performance lies in combining AOT and JIT techniques, ensuring both fast startup and optimal runtime execution.

