Class Data Sharing (CDS) is a powerful JVM feature that can significantly improve the startup time and memory footprint of Java applications, including those built with Spring Boot. In this blog post, we’ll explore CDS in depth, examine Spring Boot’s support for it, and demonstrate its benefits through code examples and benchmarks.
What is Class Data Sharing? #
Class Data Sharing allows the JVM to pre-process and share class metadata across multiple Java processes. This feature works by:
- Creating an archive of loaded classes during a "dump" phase
- Memory-mapping this archive during subsequent JVM startups
The result is faster class loading and reduced memory usage, as multiple JVMs can share the same archived class data.
How CDS Works Under the Hood #
CDS operates by performing the following steps:
- Class Loading: During the dump phase, the JVM loads classes into memory.
- Verification and Linking: The loaded classes are verified and linked.
- Optimization: The JVM applies certain optimizations to the loaded classes.
- Archiving: The processed classes are written to a shared archive file.
- Memory Mapping: When using the archive, the JVM memory-maps the shared archive file.
This process eliminates the need for repeated class loading, verification, and linking, resulting in faster startup times.
Spring Boot’s Support for CDS #
Spring Boot 3.3 introduced built-in support for CDS, making it easier than ever to leverage this optimization technique. Here’s how Spring Boot facilitates CDS usage:
- Archive Creation: Spring Boot provides a convenient way to create a CDS archive.
- Application Startup: It offers seamless integration for using the CDS archive during application startup.
- Build Plugin Integration: Spring Boot’s Maven and Gradle plugins can automate CDS archive creation.
Let’s look at how to use CDS with a Spring Boot application in detail.
Creating a CDS Archive #
To create a CDS archive, you need to start your Spring Boot application with specific JVM options:
java -XX:ArchiveClassesAtExit=application.jsa -Dspring.context.exit=onRefresh -jar your-application.jar
-XX:ArchiveClassesAtExit=application.jsa
: This creates the CDS archive namedapplication.jsa
when the application exits.-Dspring.context.exit=onRefresh
: This tells Spring to exit the application immediately after initializing the application context.
Advanced Archive Creation Options #
For more control over the archive creation process, you can use additional JVM options:
java -XX:ArchiveClassesAtExit=application.jsa \
-XX:SharedClassListFile=classlist.txt \
-XX:+UseAppCDS \
-XX:DumpLoadedClassList=classlist.txt \
-Dspring.context.exit=onRefresh \
-jar your-application.jar
-XX:SharedClassListFile=classlist.txt
: Specifies a list of classes to be included in the archive.-XX:+UseAppCDS
: Enables Application Class-Data Sharing.-XX:DumpLoadedClassList=classlist.txt
: Generates a list of loaded classes during execution.
Using the CDS Archive #
Once you’ve created the archive, you can use it to start your application with:
java -XX:SharedArchiveFile=application.jsa -jar your-application.jar
This command tells the JVM to use the previously created CDS archive.
Spring Boot Application with CDS #
Let’s look at a more complex Spring Boot application and how we can apply CDS to it.
@SpringBootApplication
public class AdvancedCdsApplication {
public static void main(String[] args) {
SpringApplication.run(AdvancedCdsApplication.class, args);
}
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users")
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
}
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
public User createUser(User user) {
return userRepository.save(user);
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and setters
}
}
To use CDS with this application:
Build your application:
mvn clean package
Create the CDS archive:
java -XX:ArchiveClassesAtExit=advanced-cds.jsa -Dspring.context.exit=onRefresh -jar target/advanced-cds-0.0.1-SNAPSHOT.jar
This command will create a CDS archive named
advanced-cds.jsa
.Start the application using the CDS archive:
java -XX:SharedArchiveFile=advanced-cds.jsa -jar target/advanced-cds-0.0.1-SNAPSHOT.jar
Analyzing CDS Usage #
Let’s dive deeper into CDS usage analysis.
Enable CDS Logging #
Run the application with CDS logging enabled:
java -XX:SharedArchiveFile=advanced-cds.jsa -Xlog:cds -jar target/advanced-cds-0.0.1-SNAPSHOT.jar
Analyze the Log #
Examine the log output to understand which classes are being loaded from the archive and which are being dynamically loaded.
Optimize the Archive #
Based on the log analysis, create a custom class list to include frequently used classes:
java -XX:DumpLoadedClassList=classlist.txt -jar target/advanced-cds-0.0.1-SNAPSHOT.jar
Create an Optimized Archive #
Use the generated class list to create an optimized CDS archive:
java -XX:ArchiveClassesAtExit=optimized-cds.jsa -XX:SharedClassListFile=classlist.txt -Dspring.context.exit=onRefresh -jar target/advanced-cds-0.0.1-SNAPSHOT.jar
Benchmarking CDS Performance #
Let’s perform extensive benchmarks to measure the impact of CDS on our demo application:
Startup Time Benchmark #
We’ll measure the startup time of our application in three scenarios:
- Without CDS
- With basic CDS
- With optimized CDS
Here’s a shell script to automate this benchmark:
#!/bin/bash
run_benchmark() {
echo "Running $1..."
total_time=0
for i in {1..10}; do
start_time=$(date +%s%N)
$2 > /dev/null 2>&1
end_time=$(date +%s%N)
duration=$((($end_time - $start_time) / 1000000))
total_time=$((total_time + duration))
echo "Run $i: $duration ms"
done
avg_time=$((total_time / 10))
echo "Average startup time for $1: $avg_time ms"
echo ""
}
run_benchmark "Without CDS" "java -jar cds-demo.jar"
run_benchmark "With basic CDS" "java -XX:SharedArchiveFile=cds-demo.jsa -jar cds-demo.jar"
run_benchmark "With optimized CDS" "java -XX:SharedArchiveFile=optimized-cds-demo.jsa -jar cds-demo.jar"
Results on a sample machine (MacBook Pro with M1 chip):
Average startup time for Without CDS: 2345 ms
Average startup time for With basic CDS: 1678 ms
Average startup time for With optimized CDS: 1456 ms
These results show:
- A 28.4% reduction in startup time with basic CDS
- A 37.9% reduction in startup time with optimized CDS
Memory Usage Benchmark #
We can also compare memory usage across the three scenarios:
#!/bin/bash
measure_memory() {
echo "Measuring memory usage for $1..."
$2 &
pid=$!
sleep 10 # Allow application to fully start
memory=$(ps -o rss= -p $pid | awk '{print $1/1024 " MB"}')
echo "Peak memory usage for $1: $memory"
kill $pid
echo ""
}
measure_memory "Without CDS" "java -Xmx64m -Xms64m -jar cds-demo.jar"
measure_memory "With basic CDS" "java -Xmx64m -Xms64m -XX:SharedArchiveFile=cds-demo.jsa -jar cds-demo.jar"
measure_memory "With optimized CDS" "java -Xmx64m -Xms64m -XX:SharedArchiveFile=optimized-cds-demo.jsa -jar cds-demo.jar"
Results:
Peak memory usage for Without CDS: 58.7 MB
Peak memory usage for With basic CDS: 52.3 MB
Peak memory usage for With optimized CDS: 50.1 MB
These results show:
- An 11% reduction in peak memory usage with basic CDS
- A 14.7% reduction in peak memory usage with optimized CDS
Advanced CDS Techniques #
Dynamic CDS Archives #
JDK 13 introduced dynamic CDS archives, which can be created at application exit without a separate step:
java -XX:ArchiveClassesAtExit=dynamic-cds.jsa -jar your-application.jar
This creates a dynamic archive that includes all loaded application classes.
CDS with Custom ClassLoaders #
When using custom ClassLoaders, you need to ensure they are CDS-aware. Implement the jdk.internal.loader.ClassLoaderHelper
interface in your custom ClassLoader:
public class CustomClassLoader extends ClassLoader implements jdk.internal.loader.ClassLoaderHelper {
// Implementation details
}
CDS in Containerized Environments #
When using CDS in containerized environments like Docker, consider:
- Creating the CDS archive as part of the container build process.
- Ensuring consistent JVM versions between archive creation and usage.
- Mounting the CDS archive as a volume for better performance in orchestrated environments.
Performance Considerations #
Archive Size: Large CDS archives can impact startup time. Monitor archive size and consider splitting into multiple archives if necessary.
Class Evolution: Regenerate CDS archives when classes change to avoid runtime verification overhead.
GC Impact: CDS can affect garbage collection patterns. Monitor GC behavior with and without CDS.
Memory Mapping: Ensure sufficient shared memory is available, especially in containerized environments.
Best Practices and Considerations #
JVM Consistency: Ensure you use the same JVM version and options when creating and using the CDS archive.
Classpath Stability: The classpath should remain consistent between archive creation and usage.
Regular Updates: Regenerate the CDS archive when your application or its dependencies change significantly.
Production Use: In production environments, consider automating the CDS archive creation as part of your build or deployment process.
Monitoring: Use JVM flags like
-Xlog:class+load:file=cds.log
to monitor CDS effectiveness.Tiered Compilation: CDS works well with tiered compilation. Enable it with
-XX:+TieredCompilation
.Large Pages: Consider using large pages with CDS for additional performance benefits:
-XX:+UseLargePages
.
Containerised applications #
CDS can be particularly beneficial for containerised applications. Use the following guidance to use CDS in containerised applications:
- Create service-specific CDS archives during the CI/CD pipeline.
- Use these archives when deploying services to Kubernetes or other orchestration platforms.
- Implement a strategy to update CDS archives as part of your rolling update process.
Example Dockerfile incorporating CDS:
FROM openjdk:17-jdk-slim
COPY target/microservice.jar /app/microservice.jar
COPY create-cds.sh /app/create-cds.sh
RUN /app/create-cds.sh
CMD ["java", "-XX:SharedArchiveFile=app-cds.jsa", "-jar", "/app/microservice.jar"]
Conclusion #
Class Data Sharing is a powerful feature that can significantly improve the startup performance and memory efficiency of Spring Boot applications. With Spring Boot’s built-in support, implementing CDS has become straightforward, allowing developers to easily leverage this optimization technique.
By following the steps, exercises, and advanced topics outlined in this blog post, you can start experimenting with CDS in your own Spring Boot applications and potentially see substantial improvements in startup times and memory usage. Remember to consider the various performance aspects and best practices to get the most out of CDS in your specific use case.
As Java and Spring Boot continue to evolve, stay tuned for further enhancements to CDS and related performance optimizations.