Speed Up Your Java Apps Instantly with Java 13’s Dynamic CDS Archives
- Published on
- Authors
- Name
- Spaghetti Code Jungle
- @spagcodejungle

Speed Up Your Java Apps Instantly with Java 13’s Dynamic CDS Archives
Flip two JVM flags and let Java 13’s Dynamic CDS Archives turn cold-start spaghetti into lightning-fast launches—no code changes, just speed.
Introduction
Java’s release cadence keeps delivering small features with big real-world impact. If you care about startup time (microservices, CLIs, serverless) and memory efficiency (multi-tenant hosts, containers), Class-Data Sharing (CDS) is a low-effort win. And with Java 13’s Dynamic CDS Archives, it’s now easier than ever to capture and reuse class metadata from a real application run—no precomputed class list, no fiddly tooling.
This post demystifies CDS, shows how Dynamic CDS works, and gives you production-ready steps to enable it in local dev, CI, Docker, and Kubernetes.
What Is Class-Data Sharing (CDS)?
CDS lets multiple JVM processes share a single memory-mapped archive of class metadata (e.g., parsed bytecode, method tables). Instead of each JVM re-parsing commonly used classes on startup and keeping duplicate copies in RAM, they map the same data from a prebuilt archive.
Why it’s great
- Faster startup: Less work to load common classes.
- Lower memory footprint: Shared, read-only pages across JVMs.
- Predictability: More consistent cold starts.
The old friction
Historically, AppCDS (application CDS) required you to generate or maintain a class list ahead of time. If your classpath or app changed, you had to regenerate it—easy to skip, easy to break.
Enter Dynamic CDS Archives (Java 13)
Dynamic CDS Archives remove that friction. You run your app once with a flag that tells the JVM to record exactly which classes it really used, then save an archive at exit. On the next run, you point the JVM at that archive and reap the benefits.
What’s new vs. old AppCDS?
- No pre-generated class list. The JVM does the discovery for you during a normal run (tests, smoke run, or production run).
- Real usage = better archive. You capture the actual warm-up behavior of your app, not a guess.
How it works (conceptually)
- Start your app with
-XX:ArchiveClassesAtExit=<file>.
The JVM marks classes that became “archivable” during the run. - On exit, the JVM writes those classes into
*.jsa(Java Shared Archive). - On later launches, you pass
-XX:SharedArchiveFile=<file>so the JVM maps that archive on startup.
Quick Start: Enable Dynamic CDS Archives
Works on Java 13+ (feature introduced in JEP 350). Replace
yourapp.jarandcom.yourapp.Mainwith your actual values.
Step 1 — Record a real run (local dev or CI)
# Run your app/test to exercise typical startup paths
java -Xshare:on \
-XX:ArchiveClassesAtExit=app-cds.jsa \
-cp yourapp.jar com.yourapp.Main
Tip: Use your smoke test or a representative integration test so commonly used code paths are covered.
Step 2 — Start using the generated archive
java -Xshare:on \
-XX:SharedArchiveFile=app-cds.jsa \
-cp yourapp.jar com.yourapp.Main
That’s it. If CDS loads successfully, you should see shorter cold starts and reduced RSS when multiple JVMs run on the same host.
Verify It’s Working (and Troubleshoot)
Add logging to see exactly what CDS is doing:
java -Xshare:on \
-XX:SharedArchiveFile=app-cds.jsa \
-Xlog:cds=info \
-cp yourapp.jar com.yourapp.Main
Common checkpoints:
Archive loaded? Look for cds log lines like Opened shared archive file and Relocated ….
Classpath mismatch? If your classpath/jars change, the JVM may skip some archived entries. Re-record the archive after dependency changes.
JDK mismatch? Archives are JDK-build specific. Re-generate if you upgrade the JDK.
Require success: Use -Xshare:on to fail if sharing can’t be enabled (useful in CI).
Why You Should Care
Immediate performance gains with near-zero effort.
Better memory sharing across JVM instances on the same node.
No code changes or library swaps—just two runtime flags.
Real-World Scenarios
1) Microservices cold starts
Containerized services that scale to zero or scale out rapidly benefit the most. With CDS, instances map shared metadata and skip redundant class parsing, shaving precious milliseconds to seconds from startup.
2) Cloud cost optimizations
If you run many identical JVMs per node, shared read-only sections reduce total memory pressure, letting you pack more pods per node or choose smaller instance sizes.
3) Local dev productivity
Developers restarting apps/tests frequently get faster feedback loops. Dynamic CDS “just works” without maintaining a classlist.
CI/CD Integration (Recommended)
Bake archive creation into your pipeline so it’s always fresh:
# 1) Build your app (e.g., Gradle/Maven)
mvn -B -DskipTests clean package
# 2) Exercise startup path (smoke test)
java -Xshare:on \
-XX:ArchiveClassesAtExit=app-cds.jsa \
-jar target/yourapp.jar --smoke
# 3) Publish artifacts
# - target/yourapp.jar
# - app-cds.jsa
If you use Spring Boot, your jar will likely be target/yourapp-<version>.jar. You can also use a wildcard:
java -Xshare:on -XX:ArchiveClassesAtExit=app-cds.jsa -jar target/*.jar --smoke
Runtime (staging/prod):
java -Xshare:on \
-XX:SharedArchiveFile=/opt/app/app-cds.jsa \
-jar /opt/app/yourapp.jar
Optional: Maven Exec for Local “Smoke” Run
If you run via a Main class instead of a fat jar:
pom.xml (snippet)
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>com.yourapp.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
Record archive using exec:
mvn -q -DskipTests package
java -Xshare:on \
-XX:ArchiveClassesAtExit=app-cds.jsa \
-cp target/classes:$(dependency:list -DincludeScope=runtime -DoutputAbsoluteArtifactFilename | awk -F': ' '/ -> /{print $2}' | paste -sd:) \
com.yourapp.Main --smoke
(Simpler path: prefer the fat/uber jar so you can use -jar target/*.jar.)
Docker & Kubernetes
Dockerfile
FROM eclipse-temurin:17-jre
WORKDIR /opt/app
# Copy app jar and the CDS archive baked by CI
# If your jar name varies, use a multi-stage build or ARG
COPY target/yourapp.jar /opt/app/yourapp.jar
COPY app-cds.jsa /opt/app/app-cds.jsa
ENV JAVA_OPTS="-Xshare:on -XX:SharedArchiveFile=/opt/app/app-cds.jsa -Xlog:cds=info"
CMD ["sh", "-c", "java $JAVA_OPTS -jar /opt/app/yourapp.jar"]
CMD ["sh", "-c", "java $JAVA_OPTS -jar /opt/app/app.jar"]
Multi-stage build (Optional)
FROM eclipse-temurin:17-jdk as build
WORKDIR /src
COPY . .
RUN mvn -B -DskipTests clean package
FROM eclipse-temurin:17-jre
WORKDIR /opt/app
COPY /src/target/yourapp.jar /opt/app/yourapp.jar
# Option A: record at runtime in staging to generate the archive
# Option B: generate in CI and COPY it here:
# COPY app-cds.jsa /opt/app/app-cds.jsa
ENV JAVA_OPTS="-Xshare:on -Xlog:cds=info"
CMD ["sh", "-c", "java $JAVA_OPTS -jar /opt/app/yourapp.jar"]
Quick Commands (Maven)
Record once:
mvn -B -DskipTests clean package
java -Xshare:on -XX:ArchiveClassesAtExit=app-cds.jsa -jar target/yourapp.jar
Then run faster:
java -Xshare:on -XX:SharedArchiveFile=app-cds.jsa -jar target/yourapp.jar
Kubernetes (snippet)
spec:
containers:
- name: yourapp
image: yourorg/yourapp:latest
resources:
requests:
memory: "256Mi"
limits:
memory: "512Mi"
Because multiple pods generally don’t share memory pages across containers on different nodes, the biggest memory win is when multiple JVMs run on the same host (e.g., sidecars, multiple workers per node). You still keep the startup win either way.
Practical Tips & Gotchas
Warm what you care about. When recording, run a path that resembles real startup (e.g., Spring context init, config loading, auth filters).
Spring/Framework notes. Heavy reflection proxies can still be archived if resolved during the run; if not, they’ll just behave as usual—no harm.
Keep archives close. Put app-cds.jsa next to your jar and version it alongside the build to avoid drift.
Fallback gracefully. If you prefer not to fail on missing archives, use -Xshare:auto (default). Use -Xshare:on in CI to catch regressions.
Measure. Pair with -Xlog:safepoint/-Xlog:cds and external timers to confirm the startup delta in your environment.
Benchmarks: How to Measure Your Gain
# naive stopwatch
time java -Xshare:on \
-XX:SharedArchiveFile=app-cds.jsa \
-jar app.jar --help
# or a simple script to run N times cold (clearing OS cache optional)
for i in {1..5}; do
/usr/bin/time -f "%E real, %U user, %S sys" \
java -Xshare:on -XX:SharedArchiveFile=app-cds.jsa -jar app.jar >/dev/null
done
Expect biggest wins on cold starts and for apps/frameworks that load sizeable sets of classes early.
FAQ
Q: Do I need Java 13 exactly? A: Dynamic CDS was introduced in Java 13 and is available in later releases (e.g., 17, 21). Use the same (or compatible) JDK to create and consume the archive.
Q: What if my jar set changes? A: Re-record the archive. The JVM can still start without it, but you’ll lose some sharing.
Q: Can I ship one archive per environment? A: Yes—treat it like a build artifact. Many teams rebuild archives in CI per commit or release.
Final Thoughts
Java keeps getting smarter, and so should your builds. Dynamic CDS Archives are a two-flag optimization that pays off immediately: quicker cold starts, lower memory, and zero code churn. Add archive creation to your CI today, package it with your image, and enjoy the calm confidence of faster Java.
Next step: Try it now—
# Record once
java -Xshare:on -XX:ArchiveClassesAtExit=app-cds.jsa -jar yourapp.jar
# Then run faster
java -Xshare:on -XX:SharedArchiveFile=app-cds.jsa -jar yourapp.jar
Happy shipping! 💻 ⚒️