In GraalVM native image, what is the difference between --initialize-at-build-time and --initialize-at-run-time?
answer
- build-time = run static init during compile, snapshot into image heap
- run-time = defer init to app startup, fresh each launch
- build-time bakes in env/secrets/random-seed/timestamp
- SecureRandom forced to run-time
- 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 sGraalVM 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 linespublic 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
Know the one-line distinction: build-time runs the static initializer during compilation and freezes the result; run-time defers it to app startup.
Explain the image heap snapshot and name concrete hazards (env vars, timestamps, random seeds) captured by build-time init.
Discuss how Spring AOT + reachability metadata set defaults, how to override via native-image.properties, and the closed-world rationale for build-time init.
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.