BaoKhangIf you run Java workloads on Kubernetes, you have probably stared at slow pod readiness metrics...
If you run Java workloads on Kubernetes, you have probably stared at slow pod readiness metrics during a deployment or autoscaling event. While the JVM is unmatched for long-running throughput, its cold-start tax—spent loading, verifying, and linking thousands of classes—can make scaling painful.
The usual advice is to compile your application into a GraalVM Native Image. That delivers sub-second startups, but it comes at a steep operational price: long build times, closed-world reflection constraints, and breaking changes to existing Java agents and libraries.
OpenJDK offers a middle ground: Application Class-Data Sharing (AppCDS) on JDK 21+ and Project Leyden AOT caches on JDK 25+. These features let the JVM dump its internal class metadata into a pre-linked archive file on disk, allowing subsequent restarts to memory-map the classes instead of parsing them from scratch.
When I started building mnemo-warmup—an open-source tool that automates training and sharing AppCDS/Leyden caches inside Kubernetes without changing application source code—I wanted to answer a simple engineering question:
Does a Kubernetes pod actually become Ready sooner once you pay the infrastructure tax of fetching, verifying, and mounting the cache?
After running hundreds of automated cold-restart benchmarks across different JVM architectures, the answer turned out to be: It depends entirely on how you deliver the tooling, and what your application does during its boot cycle.
In a standard Kubernetes setup, injecting a shared cache archive into a stateless pod requires a familiar pattern:
emptyDir volume.On paper, this looks clean. In practice, measuring the end-to-end timeline from pod.status.startTime to Ready revealed a major bottleneck.
In our initial benchmarks running a heavy Spring Boot application (69 JARs, constrained to 1 CPU / 1 GiB), the AppCDS archive successfully cut the JVM's internal startup time from 3.18s down to 1.88s. Yet, total pod readiness remained completely unchanged at 5 seconds.
Even after rewriting our helper utilities from Java into static Go binaries (dropping tool execution time from ~1,050ms down to ~140ms), the pod was still losing nearly 1.75 seconds before the main application even began. The culprit wasn't our code—it was Kubernetes itself. Because the kubelet schedules, initializes, and tears down init containers sequentially, the orchestration overhead was eating 100% of our JVM startup gains.
To eliminate the init-container tax, we pivoted to Kubernetes image volumes (introduced as beta in Kubernetes 1.33+ and supported by containerd 2.1+).
Instead of spinning up sequential containers to copy binaries around, an image volume allows the container runtime to mount the read-only OCI tools layer directly into the application container's filesystem at launch. Our static launcher runs as the entrypoint, fetches the archive directly, and replaces itself with java via exec so the JVM remains PID 1.
Removing the init containers changed the economics overnight:
| Workload Profile (1 CPU / 1 GiB) | Plain Deployment (none) |
Init Containers (cached) |
Image Volume (cached) |
Net Pod-to-Ready Delta |
|---|---|---|---|---|
| 🟢 Spring Boot (69 JARs) | 5 s | 5 s | 3 s | -2 s (40% faster) |
| 🟡 Jenkins LTS (2.492.3) | 4 s | 5 s | 4 s | 0 s (Parity) |
| 🔴 SonarQube (26.11) | 15 s | 17 s | Not Run | +2 s (Slower) |
(Note: Figures are medians over 12 cold restarts on a local kind cluster; Kubernetes pod condition timestamps have a 1-second resolution.)
Testing across three distinct real-world applications mapped out the exact boundaries of where shared class caches thrive and where they fail.
Modern microservice frameworks spend the vast majority of their boot cycle scanning classpaths, evaluating conditional annotations, and weaving dependency graphs. On our heavy Spring Boot workload, AppCDS cut JVM initialization by over 40%. Once paired with image volume delivery, total pod readiness dropped from 5 seconds to 3 seconds—with zero changes to the application's source code.
Jenkins is a massive Java monolith, making it seem like an ideal candidate for AppCDS. However, while the archive successfully loaded 85% of Jenkins' classes and shaved ~300ms off the raw JVM boot, total pod readiness stayed flat at 4 seconds.
Jenkins spends most of its startup timeline performing tasks that class-data sharing cannot accelerate: unpacking bundled archives, parsing XML configurations on disk, and executing sequential plugin initialization hooks. When class loading isn't your primary bottleneck, caching won't move the needle.
SonarQube proved to be an important architectural edge case. In our test harness (run at 2 CPU / 3 GiB), training succeeded cleanly and all 12 pods validated and loaded the archive. Yet pod readiness got 2 seconds slower.
The reason lies in how SonarQube boots. Its container entrypoint launches a small Java coordinator process, which then dynamically spawns three separate child JVMs inside the container (one for ElasticSearch, one for the Web Server, and one for the Compute Engine). Because a container wrapper only passes -XX:SharedArchiveFile to the parent entrypoint command, the heavy child JVMs booted completely cold.
If you are evaluating AppCDS or Project Leyden for your Kubernetes clusters, use these three rules to predict your ROI:
image volumes.mnemo-warmup
mnemo-warmup is currently in active alpha. Before tagging our v0.3.0 beta, we are running the next wave of studies: comparing JDK 21 AppCDS against JDK 25 Project Leyden AOT archive sizes, mapping out the application size break-even curve (from 40 MB microservices to 350 MB monoliths), and simulating HPA scale-out bursts against remote cloud registries.
If you want to inspect the raw benchmark CSVs, reproduce the cluster tests on your own hardware, or weigh in on making image volumes our default delivery mode, check out the repository at mnemo-warmup.
mnemo-warmup and when is it a good fit?
mnemo-warmup is a Kubernetes-native tool that automatically trains and shares OpenJDK AppCDS (.jsa) and Project Leyden AOT (.aot) caches across application pods without requiring source code changes or GraalVM compilation.
mnemo-warmup compare to GraalVM Native Image and build-time AOT?
mnemo-warmup keeps the standard HotSpot JVM, requires zero code changes, and preserves full dynamic Java compatibility while still cutting Spring Boot pod readiness by ~40%.onRefresh in CI): Baking a cache into a Docker image during CI causes fingerprint drift if production Kubernetes manifests modify -Xmx, GC flags, or environment variables. mnemo-warmup trains asynchronously inside the cluster using the exact live pod fingerprint.mnemo-warmup recommend Kubernetes image volumes over init containers?
Standard Kubernetes init containers (init-container mode) introduce ~1.75 seconds of sequential kubelet scheduling and container startup overhead, which cancels out the JVM startup savings on fast-booting microservices. Setting warmup.mnemo.io/tools-delivery: "image-volume" (supported on Kubernetes 1.33+ and containerd 2.1+) mounts the tools directly as a read-only OCI layer, eliminating init containers and dropping Spring Boot Pod-to-Ready times from 5 seconds to 3 seconds.
No. mnemo-warmup is strictly fail-safe. It never passes -Xshare:on (which causes fatal JVM exits on cache mismatch). If the OCI registry is unreachable, the archive digest is corrupted, or a JVM flag changes, the launcher logs the mismatch reason and falls back to an ordinary cold JVM start with exit code 0.
mnemo-warmup require a Kubernetes Operator, CRD, or mutating webhook?
No. It operates entirely via a stateless CLI (mnemo-warmup inject) used as a Helm 3 post-renderer or GitOps manifest transformer. Adding three pod template annotations (warmup.mnemo.io/enabled, warmup.mnemo.io/store, warmup.mnemo.io/exit-mode) injects the required volume mounts and generates a standard Kubernetes CronJob and Lease for asynchronous training.
mnemo-warmup?
OpenJDK invalidates AppCDS and Leyden AOT archives if the underlying JAR file modification times (mtime) or paths differ between training and runtime. Extracting a Spring Boot 3.3+ fat JAR at container build time (java -Djarmode=tools -jar app.jar extract) ensures the trainer pod and application replicas read identical, immutable file layouts.