JFR is an event-based diagnostic and profiling framework that can be used to capture information about the JVM, JDK, and your applications. It can be used for both debugging purposes as well as comparative analysis to measure the impact of optimizations. To demonstrate this, we’ll use Ahead-of-Time (AOT) computation, a performance feature being developed under Project Leyden.
JDK 24 introduced the Ahead-Of-Time (AOT) cache via JEP 483: Ahead-of-Time Class Loading & Linking. This is a HotSpot JVM feature that caches classes after they are read, parsed, loaded, and linked. Creating an AOT cache is specific to an application, and you can reuse it in subsequent runs of that application to improve the time to the first functional unit of work (start up time) and time to peak performance (warm up time).
A training run captures application configuration and execution history for use in subsequent test and production runs. Let's explore how to use JFR to measure performance optimizations while getting familiar with AOT caching!
JFR uses profiles configured in .jfc files to control the types and conditions under which JFR events are captured and information collected. These are standard XML files, with a specific schema that JFR understands. There are 2 JFR profiles shipped with the JDK:
default.jfcthat has less than 1% overhead and is designed for continuous recording ("always on")profile.jfccan have slightly more overhead (<2%) due to increasing the amount of information it is capturing.
- Check where those two profiles are located by running the following commands in a terminal window:
# windows compatible command
dir "%JAVA_HOME%\lib\jfr"
# macOS and Linux compatible command
ls "$JAVA_HOME/lib/jfr"- Open
default.jfcin a text editor and search forjdk.ClassLoadevent. - Check if the event is enabled or not. If not enabled, you can run the following command in the terminal window:
# Configure the event and write a settings file.
#
# Note: the output file is written relative to your current working directory.
$ jfr configure 'jdk.ClassLoad#enabled=true' --output custom.jfcRunning a command like the one above should create a new file named custom.jfc in your current working directory.
This new file contains a configuration based on default.jfc, but with the jdk.ClassLoad event enabled. Open the file and double check that the event is enabled.
Tip
JFR also has a 12-step interactive wizard, which can walk you through creating a new .jfc. This can provide more detail on the various options for configuring JFR and the impacts those options might have on performance. Accepting all the defaults creates a .jfc file that is equivalent to default.jfc. Give it a shot by running $ jfr configure --interactive in the terminal window.
- To capture which classes are loaded, you should start a JFR recording when you launch your application by running the following command in the terminal while in the
spring-petclinicdirectory:
$ java -XX:StartFlightRecording=settings=../custom.jfc,dumponexit=true,filename=recording-no-cache.jfr -jar target/spring-petclinic-4.0.0-SNAPSHOT.jar- Note the value you see for start up time (line containing
Started PetClinicApplication in) of the application here. - Stop the application.
- After shutting down the application, use the command:
$ jfr summary recording-no-cache.jfrThe top line in the report should be jdk.ClassLoad; the first number is the number of times that event fired during the runtime of the application. Make a note of this number.
Tip
The jfr tool does a good job of providing help for how to use an option. If you are unsure on how to use an option add --help to the end of it like this: $ jfr print --help to see what's available and provide usage instructions.
The next step after observing which classes are loaded at runtime is to see how the AOT cache takes class loads into account. Let's walk through the process of creating and using an AOT cache.
- With JDK 25, JEP 514 introduced the 1-step workflow for creating an AOT cache with the
-XX:AOTCacheOutputJVM argument. Let's use it to execute our training run and build our cache by running this command from your terminal:
$ java -XX:AOTCacheOutput=app.aot -XX:StartFlightRecording=settings=../custom.jfc -jar target/spring-petclinic-4.0.0-SNAPSHOT.jar Tip
It's important to have your training runs match production as closely as possible. While we aren't going to review the JFR recording from our training run, we do want JFR enabled, so the classes and behavior of JFR will be included in the generated AOT cache.
- Stop the application once it has finished starting up. Note the AOT cache creation process on JVM shutdown.
Tip
The one-step process for creating an AOT cache has increased memory needs. A second JVM, with identical settings, is created to build the cache once the shutdown process for the first JVM is started. You can see this in the output with the logging statement Launching child process. If this might be an issue in your production environment, you can follow the two-step cache creation process here: https://openjdk.org/jeps/514#Motivation
- With the AOT cache created, let's use it improve the start up performance for our application by telling it to use the cache with
-XX:AOTCache=[cache-name]:
$ java -XX:AOTCache=app.aot -XX:StartFlightRecording=settings=../custom.jfc,dumponexit=true,filename=recording-with-cache.jfr -jar target/spring-petclinic-4.0.0-SNAPSHOT.jar-
Note the start up time
Started PetClinicApplication inand compare it to the initial run. It should be reduced by 20-30%. -
Stop the application.
-
After shutting down the application, use
jfr summaryagain:
jfr summary recording-with-cache.jfrAnd compare the count of jdk.ClassLoad events to the number from the first run. It should be about 30% fewer!
You can run your application using the executable jar, but loading the classes from nested jars has an additional start up cost.
Depending on the size of the jar, running the application from an exploded structure is faster and recommended in production.
The extract step is needed because Spring Boot uses a custom class loader to support jars in jars. Custom classloaders do not get the same AOT cache treatment as the built-in classloaders, but this may be something that changes in the future.
# exec in a terminal window opened with `spring-petclinic` as working directory
$ java -Djarmode=tools -jar target/spring-petclinic-4.0.0-SNAPSHOT.jar extract --destination extractedThe tree of the working directory (spring-petclinic) should like the one below:
├── LICENSE.txt
├── README.md
├── app.aot
├── build.gradle
├── custom.jfc
├── docker-compose.yml
├── extracted
│ ├── application
│ │ └── spring-petclinic-4.0.0-SNAPSHOT.jar
│ ├── dependencies
│ │ └── lib
│ ├── snapshot-dependencies
│ └── spring-boot-loader
├── gradle
├── gradlew
├── gradlew.bat
├── k8s
├── mvnw
├── mvnw.cmd
├── pom.xml
├── recording.jfr
├── settings.gradle
├── src
└── targetNow run through the process of creating and using an AOT cache we covered in the previous two labs, and compare the performance of using a cache against an extracted jar versus an "uber" jar.