rgoussu@goussu: ~/library/java/jvm-implementations
~/library/java/jvm-implementations cat graalvm.md

GraalVM deep dive

# The Graal JIT, Truffle polyglot layer, and Native Image AOT — closed-world compilation, its wins, costs, and framework support.

Conceptsaved 2026-08-09 #java#jvm#graalvm#native-image#aot

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