Overview
GraalVM is a HotSpot-based JDK whose distinguishing pieces are a JIT compiler written in Java (Graal, replacing C2), the Truffle framework for running other languages on the JVM, and — the reason most teams reach for it — Native Image: ahead-of-time compilation of a Java application into a standalone native binary that starts in milliseconds and runs in a fraction of the memory. Native Image buys those wins by giving up the open-world dynamism the JVM normally guarantees, which is why it reshapes framework design rather than being a drop-in flag.
Key points
- Graal compiler: a JIT written in Java replacing C2 in the tiered pipeline; easier to extend than C2's C++, strong partial escape analysis; also compiles Native Image.
- Truffle: interpreter framework with partial evaluation — write an AST interpreter for a language (JS, Python, Ruby…) and Graal derives compiled code; polyglot embedding.
- Native Image = closed world: all reachable code must be known at build time; a points-to analysis walks the call graph from the entry point and strips the rest.
- Build-time initialization: chosen classes run static initializers during the build, state snapshotted into the image heap — faster start, classic stale-state bug source.
- Reachability metadata: reflection, JNI, resources, proxies and serialization must be declared in JSON config (or collected via the tracing agent) — the number-one source of "works on JVM, breaks native" issues.
- The trade: startup in ~tens of milliseconds and a fraction of JVM RSS, against lower peak throughput than a warmed-up JIT (narrowed by PGO) and minutes-long, memory-hungry builds.
- Fits: CLIs, serverless/scale-to-zero, high-density Kubernetes. Doesn't fit: long-lived throughput-critical services, heavy runtime dynamism (agents, bytecode generation, dynamic classloading).
- Frameworks: Quarkus and Micronaut designed around build-time DI/AOT for this; Spring Boot 3 via its AOT engine; Mandrel is the Quarkus-aligned distribution.
Details
The Graal compiler and Truffle
Graal plugs into HotSpot via JVMCI (JEP 243) as the top-tier compiler, taking C2's place
behind C1. Written in Java, it is easier to modify and reason about than C2, and its
partial escape analysis is notably stronger — allocations sunk only on the paths needing
them. As a JIT it is broadly competitive with C2 (ahead on some allocation- and
abstraction-heavy code, occasionally behind); the strategic payoff is that the same
compiler serves AOT. Truffle sits on top: implement a language as an AST interpreter with
rewrite rules and partial evaluation turns interpreter + program into optimized machine
code, giving polyglot interop (Context.eval(...)) with cross-language inlining.
Native Image: how it works
native-image runs a static points-to analysis from the entry points, computing the
closed set of reachable classes, methods and fields; everything else is stripped. Classes
marked for build-time initialization execute their <clinit> in the builder JVM and
their static state is serialized into the image heap, so the binary starts with
config parsed and lookup tables built. Runtime-initialized classes initialize on first
use as usual — mixing the two wrongly (a build-time-initialized class capturing a
timestamp, a random seed, an open socket) is the classic failure mode. The output is a
standalone executable embedding a minimal runtime (Substrate VM): no classloading, no
interpreter, no JIT.
Dynamic features punch holes in the closed world and must be declared as reachability
metadata — JSON descriptors for reflection, resources, JNI, proxies and serialization,
conventionally under META-INF/native-image/. The tracing agent
(-agentlib:native-image-agent) records what a real run touches and emits the config;
libraries increasingly ship theirs in the shared
graalvm-reachability-metadata
repository, which build plugins consume automatically.
The build pipeline, and where the work lands:
flowchart TB
subgraph BT["Build time (native-image builder)"]
EP[Entry points] --> PT["Points-to analysis: closed set of reachable classes, methods, fields"]
RM["Reachability metadata (JSON config or tracing agent output)"] --> PT
PT --> SC[Unreachable code stripped]
SC --> CI["Build-time class init: static initializers run in the builder JVM"]
CI --> IH["Static state serialized into the image heap"]
end
subgraph RT["Run time"]
EXE["Standalone executable with Substrate VM embedded (no classloading, interpreter, or JIT)"]
RI["Runtime-initialized classes init on first use"]
end
IH --> EXE
EXE --> RI
Performance profile
- Startup: milliseconds to first request — no classloading, no warm-up; state pre-built in the image heap.
- Memory: substantially lower RSS — no JIT, no profiling data, no metadata for unreachable code.
- Peak throughput: below a warmed-up HotSpot C2/Graal JIT by default, because AOT cannot speculate on runtime profiles. Profile-guided optimization (build an instrumented image, run a representative workload, rebuild with the profile) claws much of it back — Oracle GraalVM only.
- GC: Serial GC by default; G1 is available in native images (Oracle GraalVM, Linux) for larger heaps and throughput; Epsilon for run-once processes.
- Build cost: minutes of wall time and gigabytes of RAM per image — it moves work from every start-up to each build. CI needs sizing accordingly.
When it fits — and when not
Fits: CLIs (instant start, single-file distribution), serverless and scale-to-zero (cold-start dominated), Kubernetes at density (RSS per pod is the bill), short-lived jobs. Doesn't fit: long-lived services whose cost is peak throughput on a warm JVM; anything relying on runtime bytecode generation, dynamic agents, or hot-swap dynamism; codebases on libraries without reachability metadata (audit first — that is where projects stall).
Framework support and distributions
Quarkus and Micronaut were architected for this: DI, configuration and proxies are resolved at build time with little or no reflection, so images are small and metadata mostly automatic. Spring Boot (3.x) arrives via its AOT engine — bean definitions pre-computed into generated code, reflection hints emitted — with the caveat that runtime-conditional configuration is frozen at build time. Mandrel is Red Hat's downstream Native Image distribution on standard OpenJDK: no polyglot pieces, tracked to Quarkus support streams. Oracle GraalVM (GFTC licence, free to use on Oracle's terms) carries the premium bits — PGO, G1 in native image; GraalVM Community is GPL, without them.
Examples
# Build a native image from an application jar
native-image -jar app.jar \
-o app \
--no-fallback \ # fail the build rather than emit a JVM-dependent binary
--initialize-at-build-time=com.example.config \
-H:+ReportExceptionStackTraces
# Collect reachability metadata from a representative run on the JVM first
java -agentlib:native-image-agent=config-output-dir=META-INF/native-image -jar app.jar
Build plugins (org.graalvm.buildtools for Maven and Gradle: mvn -Pnative package,
gradle nativeCompile) drive the same builder and pull shared reachability metadata.
Related
- JVM implementations compared — parent comparison; where GraalVM sits against HotSpot, OpenJ9 and Zing.
- Quarkus — the framework built around build-time AOT and native-first deployment.
- Micronaut — reflection-free DI designed for exactly these constraints.
- Spring — the retrofit case: Spring Boot AOT and its build-time freezing caveats.
- Deep dive Java — JIT vs AOT in the wider JVM execution story.
Citations
[1] GraalVM Native Image documentation [2] Reachability metadata [3] Spring Boot — GraalVM Native Image support