GraalVM vs CRaC
When it comes to improving Java application startup performance, CRaC (Coordinated Restore at Checkpoint) and GraalVM Native Image represent two distinctive strategies.
CRaC (Coordinated Restore at Checkpoint) and GraalVM Native Image represent two distinct strategies for improving Java application startup performance. Although they are often grouped together, they differ substantially in execution model, optimization strategy, and operational constraints.
Existing discussions of CRaC are largely promotional, with limited availability of independent benchmarks and incomplete treatment of trade-offs. Performance claims, particularly regarding the absence of runtime overhead, are frequently presented without sufficient qualification.
This post is not a tutorial, and it is not marketing. The goal is to explain, at a high level, how CRaC and GraalVM Native Image differ conceptually, how those differences show up in benchmarks, and when one approach is likely a better fit than the other.
Prerequisites and scope
This article assumes familiarity with the JVM execution model: bytecode interpretation, JIT compilation, deoptimization, and garbage collection. The focus will be placed on startup behavior and steady-state execution speed, not on JVM internals or framework ergonomics.
The startup problem
The JVM delivers excellent peak performance, but it takes time to get there. Initialization work, followed by JIT warm-up, can dominate execution time for short-lived processes. When a service starts frequently, scales aggressively, or is billed per CPU-second, that warm-up cost becomes a real problem. CRaC and Native Image address this problem in very different ways.
Two very different ideas
GraalVM Native Image
Native Image is an ahead-of-time compiler. It turns a Java application into a native executable that does not include a JIT compiler. Most initialization work happens at build time, which results in very fast startup and no warm-up phase.
The downside is adaptability. Without runtime profiling, optimization decisions are heuristic and fixed. Native Image Enterprise Edition mitigates this with Profile-Guided Optimization (PGO), but even then, the generated binary is effectively locked into the recorded workload.
CRaC (Coordinated Restore at Checkpoint)
CRaC takes a snapshot of a running JVM and restores it later. In practice, the application is started, optionally exercised with a representative workload, and then the process is checkpointed. Restoring that snapshot is dramatically faster than re-running initialization and warm-up.
Unlike Native Image, CRaC still runs on a full JVM with a JIT compiler. After restoration, the JIT continues collecting statistics and can deoptimize and reoptimize code if the workload changes. CRaC trades predictability and simplicity for adaptability.
That distinction—fixed optimization vs adaptive optimization—is the key to understanding the rest of this post.
The Benchmark setup
To ground this discussion, several runtimes were benchmarked using spring-petclinic-rest, a small but realistic web service with a clear separation between startup and request handling.
The comparison includes:
- Azul JDK 21 and 24
- GraalVM Native Image (Community and Enterprise, with and without PGO and G1)
- CRaC snapshots taken after initialization and after sustained load
Benchmarks were run locally on Linux, with output redirected to /dev/null to reduce I/O noise. Its important to keep in mind, benchmarks are signals and not absolute truth.
Initialization time:
The initialization results are unambiguous. Regular JDKs take ~6 seconds to become ready while Native Images start in ~0.5 seconds. CRaC restores complete in ~0.05 seconds.
This is the strongest argument in favor of CRaC. Restoring a snapshot is consistently faster than executing initialization code, no matter how optimized that code is. If startup latency matters at all, CRaC is in a different league.
Warm-up and steady-state performance
Native Images have no warm-up: they run at full speed immediately. JDKs show the expected warm-up curves reaching peak performance after several seconds of sustained load.
CRaC behaves exactly like the JDK it is based on:
- A snapshot taken immediately after initialization still requires warm-up.
- A snapshot taken after sustained load resumes at (or very close to) peak performance.
One interesting observation is that CRaC snapshots taken after warm-up are slightly slower than the corresponding JDKs in steady state. The difference is small, but measurable. In other words: CRaC does have a runtime cost.
Native Image performance depends heavily on configuration:
- Community Edition is significantly slower than the JDK.
- Enterprise Edition with G1 and PGO closes much of the gap.
- Even then, peak performance typically remains below a warmed-up JDK.
short vs long runs
When flooding the service with requests for 60 seconds, CRaC snapshots taken after warm-up deliver excellent results—often outperforming Native Image and sometimes even the JDK in short runs.
However, over longer executions the JDK still wins. Given enough time, the JIT’s ability to adapt to changing code paths pays off. CRaC narrows the gap dramatically but it does not eliminate it.
This aligns perfectly with the underlying models:
- Native Image favors predictability.
- CRaC favors fast startup.
- The JDK favors long-running adaptability.
Practical limitations matter more than speed
At this point, raw performance differences are relatively small. The real deciding factors are constraints.
CRaC inherits limitations from CRIU:
- Linux-only, with root privileges required
- External resources must be carefully managed at checkpoint time
- ASLR is disabled
- Secrets and random seeds must be handled explicitly
These are not theoretical concerns, they are operational ones.
Native Image, on the other hand, imposes a closed-world assumption:
- Reflection and dynamic features require configuration
- Some JVM features are unsupported or behave differently
- Tooling shifts from JVM-centric to native tooling
Neither approach is “free”. They simply move the complexity to different places.
So which to choose?
- Choose CRaC if startup time is critical, initialization is expensive, and if its possible to tolerate operational and security constraints.
- Choose Native Image if a predictable performance is important, the processes are short-lived, and there is a willingness to work within a more constrained runtime.
- Stick with the JDK for long-running services and where a peak throughput matters more than startup latency.
In practice, the performance differences are often marginal compared to the cost of these constraints. Benchmarks help, but they rarely make the decision on their own.
Closing thoughts
CRaC is a genuinely interesting addition to the Java ecosystem. Its startup performance is impressive, and it offers a compelling alternative to ahead-of-time compilation. Native Image remains a powerful solution, especially when paired with PGO, but it makes fundamentally different trade-offs.
There is no universal winner here—only better or worse fits. Understanding why these technologies behave the way they do is far more useful than chasing the fastest bar in a graph.
Related articles:
- GraalVM – The Swiss Army Knife of Virtual Machines
- Building GraalVM Native Images with Maven and Gradle
- Building and Debugging GraalVM Native Images
Need help choosing the right Java runtime strategy? Contact our experts to discuss your performance requirements.