A low-hanging fruit that you cannot miss Link to heading

In this post from Boost startup time on JVM series we will discuss one of the easiest way to boost startup time: the Class Data Sharing JVM feature
This is a low-hanging fruit that you cannot miss because

  • it is rock solid production feature
  • it requires almost no change to the code

Table of contents Link to heading

What is Class Data Sharing (CDS) Link to heading

Class Data Sharing (CDS) is a JVM feature aimed at reducing the startup and memory footprint of multiple JVM instances running on the same host. The feature loads a default set of classes from the system Java Archive (JAR) file and stores this data in a file, which is then available as read-only metadata to multiple JVM processes.

Benefits even for single JVM process Link to heading

  • Preloaded and Pre-Parsed Classes:
    The JVM can access them in a contiguous memory block instead of performing multiple I/O operations to locate, load, and parse individual class files.
  • Efficient Memory Use:
    Loaded classes are shared among JVM processes but also JVM can load the classes in a pre-arranged order, which reduces memory fragmentation and improves cache locality.
  • Reduce JIT Compilation time
    If Ahead Of Time (AOT) processing is used CDS can bypass or reduce Just In Time (JIT) compilation time during startup, as the JVM can use pre-compiled code from the CDS archive
    For more details see what happens at startup in with JIT and HotSpot JVM

A baseline application with all batteries included 🔋 Link to heading

In order to compare performance that we can get we can use Live Kitchen a pseudo realistic Spring Boot application
This is git repository with all batteries included! you will find complete scripts and all different docker files definition described in this post.
Please let me know if you find it useful! Startup time

mvn clean package -Dmaven.test.skip=true
java -jar application/target/*.jar

...
Started LiveKitchenApplicationKt in 2.146 seconds

Memory usage

ps -p 39648 -o rss | awk 'NR>1 {print "Application memory: " $1/1024 " MB"}'
Application memory: 302.359 MB

Create step by step CDS archive on a local machine Link to heading

  1. enable AOT processing in pom.xml ( Optional )
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <executions>
        <execution>
          <id>process-aot</id>
          <goals>
            <goal>process-aot</goal>
          </goals>
        </execution>
      </executions>
   </plugin>
  1. compile application as usual
mvn clean package -Dmaven.test.skip=true
  1. create an exploded jar ( Optional )
java -Djarmode=tools -jar application/target/application-1.0.0.jar extract
  1. start application with CDS archive creation at exit
java -Dspring.aot.enabled=true -XX:ArchiveClassesAtExit=./application.jsa -Dspring.context.exit=onRefresh -jar application-1.0.0/application-1.0.0.jar

Note: The above command leverage Spring feature to exit application on refresh lifecycle, as alternative you can manually stop the application to generate the CDS archive

For convenience the complete example is available here Live Kitchen
see in particular create_application_with_cds_and_aot.sh and run_application_with_cds_and_aot.sh scripts

Results Link to heading

...
Started LiveKitchenApplicationKt in 0.965 seconds

The startup is 55% faster! …not bad! 😎

The graph below show the impact of above optional steps on startup time

startup_time comparison

… and memory consumption

memory comparison

Create the CDS archive using a container with CDS Link to heading

To create a docker container you can use similar steps done above to run the application on local machine Moreover you can consider to use jar layers or not

One option is to create a dockerfile and at image creation run the application and create the CDS archive Another option is to create CDS archive outside the docker image

I tried different options you can find different ready dockerfiles here

As example of first option there is this docker file

FROM bellsoft/liberica-openjdk-alpine:21.0.4-9-cds as reduced_optimizer

WORKDIR /app

COPY live-kitchen/application/target/*.jar myapp.jar

RUN java -Djarmode=tools -jar myapp.jar extract


FROM bellsoft/liberica-openjdk-alpine:21.0.4-9-cds

ARG DATASOURCE_URL=changeme
ARG CHEF_SERVER_URL=changeme

ENV DATASOURCE_URL=$DATASOURCE_URL
ENV CHEF_SERVER_URL=$CHEF_SERVER_URL

ENTRYPOINT ["java","-Dspring.aot.enabled=true","-Dspring.datasource.url=${DATASOURCE_URL}","-Dchefserver.host=${CHEF_SERVER_URL}","-XX:SharedArchiveFile=application.jsa", "-jar", "myapp/myapp.jar"]

COPY --from=reduced_optimizer /app ./

RUN java -Dspring.aot.enabled=true -XX:ArchiveClassesAtExit=./application.jsa -Dspring.datasource.url=${DATASOURCE_URL} -Dchefserver.host=${CHEF_SERVER_URL} -Dspring.context.exit=onRefresh -jar myapp/myapp.jar

In my case i am running both mysql both chefserver on local docker images so use docker gateway address 172.17.0.1 as a surrogate to localhost

Here is the docker build command

docker build \
  --build-arg DATASOURCE_URL=jdbc:mysql://172.17.0.1:3306/recipes \
  --build-arg CHEF_SERVER_URL=http://172.17.0.1:9992 \
  -f live_kitchen_unpackjar_cds_aot_nolayers.Dockerfile \
  -t live_kitchen_unpackjar_cds_aot_nolayers . 

The graph below shows the impact of above optional steps on container startup time

startup_time comparison

… and container memory consumption

memory comparison

To layer or not to layer, that is the question 🎭 Link to heading

We can create a layered jar in order to separate the application from its dependencies
with a command like

java -Djarmode=tools -jar myapp.jar extract --layers --launcher

Then we can execute the application with a jar launcher that leverage jar layers structure with a command like

java ...  org.springframework.boot.loader.launch.JarLauncher

On the application git repository there is the full example of docker file that uses a layered jar

Pros using layered jar Link to heading

  • Improved Build Efficiency: Layered JARs allow Docker to cache each layer separately.
  • Faster Deployment: Updating a specific layer without rebuilding the entire JAR can reduce deployment time
  • Efficient Disk and Network Usage: By reusing layers, Docker uses less disk space
  • Improved Build Efficiency: Layered JARs allow Docker to cache each layer separately.

Cons using layered jar Link to heading

  • Potentially Slower Startup: Having the JAR split into multiple layers can add overhead, as the JVM must locate and load classes across different layers, reducing some of the CDS benefits.
  • Complexity in CDS Archive Creation: To create an effective CDS archive, the class loading order should ideally match that of the final runtime. Layering can complicate this, as each layer may have its own class dependencies, leading to less efficient use of the CDS archive.
  • Increased Complexity in Configuration: Managing both CDS archives and JAR layers requires careful configuration and testing to ensure they work harmoniously

Optimization in Practice Link to heading

  • Evaluate best jdk and docker image for your need ( features like CDS , image size , architectures supported )
  • Carefully evaluate if to use layered jar
  • Leverage AOT compilation if possible
  • Create CDS Archive Inside the Docker Image it is the easier option for portability and performances

Reflections and lessons learned Link to heading

1. General performances consideration Link to heading

  • 🥇Class Data sharing it is rock solid production feature (first definition was in JDK 5)
  • 🥇Class Data sharing it requires almost no change to the code ( if we ignore make it AOT ready )
  • 🤔 Require little more complex docker image container creation or pipeline
  • 🤔 Startup increase is up to 50% but there are other options that give better performances close to 90%
  • 🤷 Using AOT preprocessing can add a futher 10% improvement, but it can be not easy to make the application AOT compliant.

2. The choice of framework and libraries have an big impact on performances Link to heading

🤔 Evaluate whether it’s worth to use libraries or frameworks like Spring boot that rely heavily on reflection, proxies, and dynamic features. This type of dynamism give a lot of flexibility and make the code more expressive but can impact startup time. Instead, consider exploring reflection-free solutions.
Example: choosing between Thymeleaf vs JStachio as template engine:
Thymeleaf is a powerful template engine, it requires reflection.
A possible alternative is JStachio, where templates are compiled into readable Java source code, allowing for static type-checking and minimizing reflection whenever possible.
Such libraries are more suitable for compile-time (Ahead of Time) optimizations, improving performance.

3. Double check that the application is the bottleneck Link to heading

Assess what is the bottleneck when starting up a new application. It can be the application or can be the time spent on download big docker images or something else. So suggestion from theory of constraint is:

  1. identify the constraint or bottleneck
  2. remove it
  3. iterate

Conclusion Link to heading

Class Data Sharing (CDS) is one of the simplest ways to improve startup time with minimal effort and low risk. It’s definitely worth trying!.
However, if you need a significantly greater speedup by an order of magnitude other options are available that may be more effective.