Ahead-of-Time (AOT) compilation and Class Data Sharing (CDS)

In Introducing Unnamed Classes and Simplified Main Methods in Java I ended up comparing the performance of direct execution of a java source file with main method by skipping the explicit javac step with the normal build and execution process javac X; java X (some assumptions made about the classpath excluded). I then went on to use Graal to compile the simple app into native code. The performance aspect I was looking at was start up and the simple CLI that did some stuff and then exited provided a reasonable exercise.

I had forgotten that there is yet another performance boost option available these days - ahead of time (AOT) compilation and class data sharing. Courtesy of Claude AI:

What is AOT Compilation?

Ahead-of-Time (AOT) compilation is a powerful performance optimisation technique in Java that helps your applications start faster and run more efficiently. Traditionally, Java uses Just-In-Time (JIT) compilation, where code is compiled during runtime. AOT flips this approach by compiling Java classes into native machine code before the application actually runs.

Why Does This Matter?

For new developers, imagine your Java application is like a car:

  • Traditional JIT Compilation: Each time you start the car, the engine needs to warm up and adjust.
  • AOT Compilation: It’s like the car is pre-tuned and ready to go immediately when you turn the key.

Key Benefits

  • Faster Startup Times: Your application launches more quickly
  • Reduced Initial Performance Overhead: Less processing needed when the app first starts
  • Consistent Performance: Eliminates the initial “warmup” period typical in JIT compilation

How AOT Works with Class Data Sharing (CDS)

AOT builds upon Class Data Sharing, a feature that allows the Java Virtual Machine (JVM) to:

  • Cache common class metadata
  • Share this metadata across multiple Java processes
  • Reduce memory footprint and startup time

Well that’s the background so how do we use it? There are a few steps so a build tool like maven and gradle are your friends, but I’ll run through it in long hand.

  1. Compile the java source as normal. Using the helloworld/figlet/banner code
    $ javac --enable-preview --source 25 -cp deps/* Helloworld.java
    
  2. Now for a training run so that the AOT compiler can profile the code execution.
    $ java --enable-preview -XX:AOTMode=record -XX:AOTConfiguration=target/app.aotconf  -cp deps/*:. Helloworld "Too cool for skool"
    

    Except that this will fail with an error about a non-empty directory.

$  java --enable-preview -XX:AOTMode=create -XX:AOTConfiguration=target/app.aotconf -XX:AOTCache=app.aot -cp deps/jfiglet-0.0.9.jar:. Helloworld "Too cool for skool"
[0.109s][error][cds] Error: non-empty directory '.'
Hint: enable -Xlog:class+path=info to diagnose the failure
Error occurred during CDS dumping
Cannot have non-empty directory in paths

Indeed directories of compiled class files are not supported by CDS but packaging as a JAR as it is intended does.

$ jar -cvf helloworld.jar Helloworld.class
added manifest
adding: Helloworld.class(in = 682) (out= 454)(deflated 33%)

$ java --enable-preview -XX:AOTMode=record -XX:AOTConfiguration=target/app.aotconf  -cp deps/*:helloworld.jar Helloworld "Too cool for skool"
  _____                             _    __                  _               _ 
 |_   _|__   ___     ___ ___   ___ | |  / _| ___  _ __   ___| | _____   ___ | |
   | |/ _ \ / _ \   / __/ _ \ / _ \| | | |_ / _ \| '__| / __| |/ / _ \ / _ \| |
   | | (_) | (_) | | (_| (_) | (_) | | |  _| (_) | |    \__ \   < (_) | (_) | |
   |_|\___/ \___/   \___\___/ \___/|_| |_|  \___/|_|    |___/_|\_\___/ \___/|_|
                                                                               
$ java --enable-preview -XX:AOTMode=create -XX:AOTConfiguration=target/app.aotconf -XX:AOTCache=app.aot -cp deps/jfiglet-0.0.9.jar:helloworld.jar Helloworld "Too cool for skool"
AOTCache creation is complete: app.aot

Now we should have the aot class cache available:

$ java -XX:AOTCache=app.aot -cp deps/jfiglet-0.0.9.jar:helloworld.jar Helloworld "Too cool for skool"       
  _____                             _    __                  _               _
 |_   _|__   ___     ___ ___   ___ | |  / _| ___  _ __   ___| | _____   ___ | |
   | |/ _ \ / _ \   / __/ _ \ / _ \| | | |_ / _ \| '__| / __| |/ / _ \ / _ \| |
   | | (_) | (_) | | (_| (_) | (_) | | |  _| (_) | |    \__ \   < (_) | (_) | |
   |_|\___/ \___/   \___\___/ \___/|_| |_|  \___/|_|    |___/_|\_\___/ \___/|_|

How long does that take to execute?

$ time java -XX:AOTCache=app.aot  -cp deps/jfiglet-0.0.9.jar:helloworld.jar Helloworld "Too cool for skool"
  _____                             _    __                  _               _
 |_   _|__   ___     ___ ___   ___ | |  / _| ___  _ __   ___| | _____   ___ | |
   | |/ _ \ / _ \   / __/ _ \ / _ \| | | |_ / _ \| '__| / __| |/ / _ \ / _ \| |
   | | (_) | (_) | | (_| (_) | (_) | | |  _| (_) | |    \__ \   < (_) | (_) | |
   |_|\___/ \___/   \___\___/ \___/|_| |_|  \___/|_|    |___/_|\_\___/ \___/|_|


real    0m0.049s
user    0m0.054s
sys     0m0.020s

That seems quite fast but how does it compare with the new scriptable java source execution, normal java execution and a Graal native image?

Execution Method Time % std. java exe
New direct java source 0m0.385s 464
Usual javac with a class 0m0.083s 100
Graal native image 0m0.017s 20
AOT/CDS 0m0.049s 60

Caveat that the snapshot times are not an average and would be affected by machine resources.