skip to content

GraalVM Native Image Fundamentals

The GraalVM constraints Spring has to work around: the closed-world assumption, reflection, dynamic proxies, resources and serialization, and build-time versus run-time initialization. Interviewers ask why native builds fail, and every answer starts here.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

23

In GraalVM native image, what is the difference between --initialize-at-build-time and --initialize-at-run-time?

level: juniorimportance: must knowfreq 70%

answer

  1. build-time = run static init during compile, snapshot into image heap
  2. run-time = defer init to app startup, fresh each launch
  3. build-time bakes in env/secrets/random-seed/timestamp
  4. SecureRandom forced to run-time
  5. override via native-image.properties Args

basics

~20 s

--initialize-at-build-time runs a class's static initializer while the image is being built, baking the resulting state into the binary. --initialize-at-run-time defers it to when the app actually starts, so it runs fresh on each launch.

solid answer

~40 s

GraalVM builds a native executable ahead of time. For each class it must decide WHEN the static initializer (`static {}` blocks and static field assignments) runs. `--initialize-at-build-time=<class>` executes that initializer during the build, on the build machine, and snapshots the resulting object state into the image heap — so at runtime the class is already initialized with baked-in values. `--initialize-at-run-time=<class>` postpones initialization until the class is first used after the process starts, exactly like a normal JVM. Build-time init makes startup faster and enables closed-world analysis, but is dangerous for anything that captures environment, secrets, random seeds, or timestamps, because those get frozen at build time. Spring's AOT support and the GraalVM reachability metadata pick sensible defaults, but you sometimes override with these flags.

code

java · 10 lines
java
public class ApiClient {
    // If ApiClient is --initialize-at-build-time, TOKEN is captured
    // from the BUILD machine's environment (likely null), and BOOT is
    // the build timestamp, not the deploy time. Both are almost always bugs.
    static final String TOKEN = System.getenv("API_TOKEN");
    static final long BOOT = System.currentTimeMillis();

    // Correct: keep this class at run-time init so the static block
    // reads the real production environment on each launch.
}

go deeper

for a junior

Know the one-line distinction: build-time runs the static initializer during compilation and freezes the result; run-time defers it to app startup.

for a middle

Explain the image heap snapshot and name concrete hazards (env vars, timestamps, random seeds) captured by build-time init.

for a senior

Discuss how Spring AOT + reachability metadata set defaults, how to override via native-image.properties, and the closed-world rationale for build-time init.

for a principal

Reason about class-initialization conflict resolution, security implications of seeded randomness across a fleet, and setting org-wide defaults (run-time for app code) plus build validation.

## The problem these flags solve GraalVM `native-image` compiles your application ahead of time into a standalone native executable. Unlike the normal JVM — which loads and *initializes* classes lazily at runtime the first time they're touched — native image must decide, for every class, whether its **static initializer** runs (a) during the build, or (b) at application startup. A **static initializer** is the code in `static { ... }` blocks plus the assignments to `static` fields. Example: ```java public class Config { static final long BUILT_AT = System.currentTimeMillis(); // runs when? } ``` ## The two flags - **`--initialize-at-build-time=com.example.Config`** — GraalVM runs `Config`'s static initializer *on the build machine, during compilation*. The resulting object graph (the value of `BUILT_AT`, any singletons created, caches populated) is serialized into the **image heap** — a pre-computed snapshot baked into the binary. At runtime the class is already initialized; its initializer never runs again. - **`--initialize-at-run-time=com.example.Config`** — GraalVM leaves the class *uninitialized* in the image. Its static initializer runs at application startup, the first time the class is used, exactly like on a normal JVM. Fresh values every launch. Both accept comma-separated class names or package prefixes, and can be repeated. ## Why build-time init exists Running initializers at build time (1) speeds startup — no init work at boot; (2) supports GraalVM's **closed-world assumption** and reachability analysis; (3) lets constant folding shrink the binary. GraalVM historically pushed much of the JDK to build-time init. ## The danger — baked-in state Whatever runs at build time captures the **build environment**, not the production one. Classic hazards: - **Baked-in singletons**: an eagerly-created singleton is frozen; all runtime instances share the build-time object, which may hold stale config. - **Captured secrets / config**: reading `System.getenv("DB_PASSWORD")` or a properties file in a static initializer captures the *build machine's* value (often empty or a placeholder), not production's. - **Seeded randomness**: `SecureRandom` or `Random` seeded/instantiated at build time produces the **same sequence on every deployed instance** — a serious security flaw. GraalVM specifically forces `SecureRandom` to run-time init to avoid this. - **Frozen timestamps**: `System.currentTimeMillis()`, `Instant.now()` captured at build become build time, not runtime. - **Open resources**: files, sockets, threads, or native handles created at build time can't be serialized into the heap and cause build errors like "Detected an instance of Random/started Thread in the image heap." ## Spring's role Spring Boot's AOT engine (`spring-boot-starter-parent` + the AOT Maven/Gradle plugin, `org.springframework.aot`) and the GraalVM **reachability metadata** (`native-image.properties`, `reachability-metadata.json`) choose defaults for framework classes. You override for your own classes via `native-image.properties`: ``` Args = --initialize-at-run-time=com.example.CryptoHelper ``` GraalVM will also **error at build time** if a class you marked build-time-init transitively pulls in a class that must be run-time-init (a "class initialization" conflict), forcing you to make an explicit decision. ## When to use which - **Build-time**: pure, deterministic, stateless constants; framework infrastructure GraalVM/Spring already validated. - **Run-time**: anything reading env vars, files, secrets, the clock, randomness, opening I/O, or holding mutable per-deployment state. Default instinct: keep **your** application classes at run-time init unless you have a proven, safe reason to move them earlier.

  • Where does the state produced by a build-time initializer physically live in the executable?
    In the image heap — a pre-computed snapshot of the object graph serialized into the binary at build time and mapped in at startup, so the class is already initialized when the process starts.
  • If a class marked build-time-init depends on a class that must be run-time-init, what happens?
    GraalVM reports a class-initialization conflict at build time and fails the build, forcing you to reclassify one of them; it won't silently guess.

context

open as a page

What is the closed-world assumption in GraalVM native image, and what does it mean for a Spring application?

level: juniorimportance: must knowfreq 62%

basics

~20 s

GraalVM analyzes your whole app at build time and includes only the code it can prove is reachable. Nothing new can be loaded at runtime, so the native executable is a fixed, self-contained set of classes and methods.

open as a page

Why are CGLIB (class-based) proxies problematic in a GraalVM native image, and what does Spring prefer instead?

level: juniorimportance: must knowfreq 62%

basics

~10 s

CGLIB creates proxy classes at runtime by generating new subclasses. A native image is closed at build time and cannot generate classes at runtime, so CGLIB proxies fail. Spring prefers JDK/interface-based proxies instead.

open as a page

Why does reflection often break when you compile a Spring application to a GraalVM native image?

level: juniorimportance: must knowfreq 70%

basics

~20 s

GraalVM native image builds a closed world: it only includes code it can see is used at build time. Reflection resolves types/methods at runtime, so unless that reflective access is declared up front, the elements are dropped and calls fail.

open as a page

In a GraalVM native image, why do classpath resources (like a .txt or .sql file loaded via ClassPathResource) sometimes fail to load at runtime, and what fixes it?

level: juniorimportance: must knowfreq 70%

basics

~20 s

GraalVM's closed-world build only bundles resources it knows about. Files loaded reflectively at runtime aren't detected, so they're dropped. You must register them (via resource-config.json or Spring ResourceHints) so the build includes them in the image.

open as a page

Why is seeding or creating randomness (e.g. SecureRandom) in a static initializer especially dangerous under build-time initialization?

level: middleimportance: must knowfreq 55%

basics

~20 s

If a random generator is created and seeded at build time, that seed is baked into the binary. Every deployed instance then produces the identical random sequence, so tokens, IDs, or keys become predictable across the whole fleet.

open as a page

What is the 'closed-world assumption' in GraalVM native image, and how does it explain reflection failures?

level: middleimportance: must knowfreq 60%

basics

~20 s

Native image assumes it can see all reachable code at build time and compiles a fixed set — nothing can be loaded or discovered later. Reflection breaks because it discovers targets at runtime, outside that fixed set.

open as a page

How does the points-to static analysis compute the reachable set, and what starts it?

level: middleimportance: should knowfreq 40%

basics

~20 s

It starts from entry points like main and registered roots, then follows every possible call and field access transitively until nothing new is discovered. Whatever it reaches is kept and compiled; the rest is removed.

open as a page

A Spring native image builds successfully but throws MissingReflectionRegistrationError at runtime. What happened and how do you fix it?

level: middleimportance: should knowfreq 30%

basics

~20 s

The points-to analysis couldn't see a reflective call, so the target's metadata was left out of the closed world. The build still succeeds; the failure appears when that code runs. Fix it by registering a reflection hint.

open as a page

For a JDK dynamic proxy to work in a native image, what must be true at build time, and how does Spring provide it?

level: middleimportance: should knowfreq 45%

basics

~20 s

The exact set of interfaces the proxy implements must be declared at build time (in proxy/reachability metadata). Spring's AOT processing scans the beans and generates these proxy hints automatically so GraalVM can create the proxy.

open as a page

Why can't the native-image builder discover getDeclaredMethod / Class.forName targets automatically, and when can it partially succeed?

level: middleimportance: should knowfreq 40%

basics

~20 s

Those calls take runtime values (a name string) that the build-time static analysis can't evaluate, so it can't know the target. It can partially succeed only when the argument is a constant it can fold at build time.

open as a page

How do you make Java resource bundles (i18n messages) work in a GraalVM native image with Spring, and how does that differ from registering a plain classpath resource?

level: middleimportance: should knowfreq 40%

basics

~20 s

Resource bundles (message .properties per locale) need their own registration, not just a file pattern, because ResourceBundle resolves by base name plus locale. Use ResourceHints.registerResourceBundle(baseName) or GraalVM's resource-config.json 'bundles' section, so all locale variants are embedded.

open as a page

How do Spring Boot AOT and GraalVM decide class-initialization defaults, and how do you override them for your own classes?

level: seniorimportance: should knowfreq 40%

basics

~20 s

GraalVM defaults application classes to run-time init and relies on reachability metadata for framework classes. Spring's AOT processing plus that metadata set safe defaults. You override per class with --initialize-at-build-time / --initialize-at-run-time, usually via a native-image.properties Args line.

open as a page

Native images have no JIT compiler. What are the practical performance consequences versus a warmed-up JVM?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Native images start instantly and use less memory because everything is compiled ahead of time. But without a JIT there is no runtime profiling, so long-running peak throughput can be lower than a warmed-up JVM.

open as a page

Contrast full-mode vs lite-mode @Configuration classes and explain the native-image implications of each.

level: seniorimportance: should knowfreq 40%

basics

~20 s

Full mode (proxyBeanMethods = true, default) CGLIB-proxies the config class so calling one @Bean method from another returns the shared singleton. Lite mode (false) skips the proxy — calls create new instances. Native image can't do the CGLIB proxy, so lite mode is native-friendly.

open as a page

What does reflect-config.json declare, and what is the difference between a class being 'included' and being 'registered for reflection'?

level: seniorimportance: should knowfreq 45%

basics

~20 s

reflect-config.json lists classes and which reflective members (constructors, methods, fields) to keep metadata for. Being 'included' means the class is in the image; being 'registered for reflection' means its member metadata is retained so reflective lookup/invocation works.

open as a page

When does a GraalVM native image need Java serialization hints (serialization-config.json / SerializationHints), and how do you register them in Spring?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Only classes actually put through Java's ObjectInputStream/ObjectOutputStream need serialization hints. GraalVM excludes serialization metadata by default. In Spring, call hints.serialization().registerType(Foo.class) in a RuntimeHintsRegistrar, or list the class in serialization-config.json.

open as a page

During a native-image migration, an eagerly-created singleton in a build-time-initialized class ends up holding stale/empty config in production. How do you diagnose and prevent this class of bug fleet-wide?

level: principalimportance: should knowfreq 25%

basics

~20 s

The singleton was constructed at build time, so it captured build-machine config and got frozen into the binary. Fix by moving the class to run-time init (or making it a runtime-created Spring bean) and prevent recurrence with policy: no environment-reading state in static holders, plus behavioral tests on the actual native binary.

open as a page

The closed world forbids runtime class loading. As a principal engineer, how do you decide whether a Spring service should be a native image, and how do you handle dynamic requirements?

level: principalimportance: should knowfreq 22%

basics

~20 s

Go native when startup and memory dominate (serverless, high density) and the app's dynamism is bounded and known at build time. Handle dynamic needs with reachability metadata, Spring AOT, and the tracing agent — or keep genuinely dynamic services on the JVM.

open as a page

A Spring service works on the JVM but a native-image build throws MissingReflectionRegistrationError only at runtime for a class loaded via Class.forName from configuration. How do you reason about, diagnose, and prevent this class of failure?

level: principalimportance: should knowfreq 30%

basics

~20 s

The class name is a runtime string, so build-time analysis never saw it and it wasn't registered. Diagnose by running the native binary/tests (or the tracing agent), then prevent it by declaring the target reflectively (hint/reflect-config) and gating native tests in CI.

open as a page

How would you design a reliable workflow to catch missing resource/serialization hints for a native image before it reaches production, given JVM tests don't exercise them?

level: principalimportance: should knowfreq 25%

basics

~20 s

Combine three layers: unit-test hints with RuntimeHintsPredicates on the JVM, use the GraalVM tracing agent to capture real accesses into config, and run an actual native-image build in CI plus a smoke test of the binary so missing-resource failures surface before prod.

open as a page

In a Kotlin Spring app targeting native image, why can final classes break proxying, and how is it addressed?

level: middleimportance: nice to knowfreq 24%

basics

~10 s

Kotlin classes/methods are final by default. CGLIB can't subclass final classes, so class-based proxies fail. The kotlin-spring (all-open) compiler plugin makes Spring-annotated classes open. Better still, use interfaces so JDK proxies apply.

open as a page

You lead a service being migrated to GraalVM native image and hitting proxy-related failures. What design and configuration strategy do you adopt to keep proxying reliable?

level: principalimportance: nice to knowfreq 28%

basics

~10 s

Design AOP/transaction targets behind interfaces so JDK proxies apply, set proxyBeanMethods = false, let Spring AOT emit proxy hints, and add RuntimeHintsRegistrar for the few proxies AOT can't infer. Avoid CGLIB/class proxies.

open as a page