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) 🤔
- Benefits, even if it’s just one JVM in the game! 💪
- A baseline application
- Create step by step CDS archive on a local machine 🖥️
- Create the CDS archive using a container with CDS 🐳
- To layer or not to layer, that is the question 🎭
- Reflections and lessons learned 🤔
- Conclusion 🔚
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.
- It was introduced in JDK 5 including only jdk classes
- In OpenJDK 10 it was extended with JEP 310: Application Class-Data Sharing
- In OpenJDK 13 it was extended with JEP 350: Dynamic CDS Archives
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
- 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>
- compile application as usual
mvn clean package -Dmaven.test.skip=true
- create an exploded jar ( Optional )
java -Djarmode=tools -jar application/target/application-1.0.0.jar extract
- 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
… and memory consumption
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
… and container memory consumption
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:
- identify the constraint or bottleneck
- remove it
- 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.