skip to content

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

level: middleimportance: must knowfreq 60%

answer

  1. open world (JVM) vs closed world (native)
  2. reachability closure computed at build
  3. frozen binary, no runtime discovery
  4. two failures: class absent vs metadata stripped
  5. declarations re-add to closed set

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.

solid answer

~40 s

The closed-world assumption means GraalVM native image determines the complete set of reachable classes, methods, and fields during the build via static reachability analysis, then compiles a fixed, self-contained executable. There is no dynamic classloading and no runtime discovery: the world is 'closed'. Reflection contradicts this — `Class.forName`, `getDeclaredMethod`, dynamic proxies all resolve targets from runtime values the analysis cannot evaluate. So reflectively-only-reached elements aren't in the reachable set and are either dropped or have their metadata stripped, producing runtime errors. To reconcile the two, you feed the builder explicit metadata (reflect-config.json / reachability hints) that names the classes and members you'll reflect on, effectively re-adding them to the closed world. Spring's AOT engine generates most of this automatically for framework patterns.

go deeper

for a junior

Knows the phrase 'closed world' and that it freezes code at build time.

for a middle

Explains reachability closure and both failure modes (class absent vs metadata stripped).

for a senior

Notes constant-folding exceptions and that third-party reflection over your types still needs your registration.

for a principal

Uses closed-world semantics to reason about the whole dynamic-feature surface (reflection, resources, proxies, serialization) and CI gating.

## Closed world vs. open world The standard **JVM** is an **open world**: at runtime you can load a class that didn't even exist at compile time, generate proxies, and reflect over anything on the classpath. The runtime is dynamic and adaptive. GraalVM **native image** deliberately trades that away for an **closed world**. During the AOT build a **reachability (points-to) analysis** starts at the entry point and computes the transitive closure of everything actually used — classes instantiated, methods called, fields accessed. Only that closure is compiled into the native binary; the rest is dead-code-eliminated. Once built, the executable is **frozen**: no new classes can be loaded, and the reflection metadata that exists is exactly what was requested at build time. This closed world is *why* native images start in milliseconds and use little memory — there's no classpath scanning, no JIT warm-up, no speculative metadata. But it is also *why* every dynamic feature (reflection, resources, JNI, serialization, dynamic proxies) must be **declared** in advance. ## How it produces reflection failures Reflection is fundamentally an **open-world API**. Consider the analysis trying to trace: ```java Method m = obj.getClass().getDeclaredMethod(methodName, argTypes); m.invoke(obj, args); ``` The analysis sees a call to `getDeclaredMethod`, but `methodName` is a runtime value — it cannot pick which method to keep reachable. So by default GraalVM does **not** retain the enumerable metadata for that type's members, and `invoke` fails at runtime with a missing-registration error. Similarly `Class.forName(str)` with a non-constant `str` leaves the target class outside the closed world. ### The two distinct failure modes 1. **Class not included** — nothing in statically-reachable code references the type, so it's absent (`ClassNotFoundException`). 2. **Class included but not registered for reflection** — the type is present (some other path uses it directly), but its constructor/method/field metadata was stripped, so reflective enumeration/invocation fails (`MissingReflectionRegistrationError` / `NoSuchMethodException`). ## Reconciling reflection with the closed world You don't disable the closed world — you *extend* it with declarations. A **`reflect-config.json`** (or programmatic reachability hints) tells the builder: "treat these classes and these members as reachable and keep full reflection metadata." The builder folds them back into the closed set. In Spring, **Spring AOT** runs during the build and inspects your bean definitions, configuration classes, `@ConfigurationProperties`, JSON-bound DTOs, JPA entities, etc., emitting the corresponding metadata automatically. The mechanism by which it registers those hints is covered elsewhere (reachability metadata); here the takeaway is conceptual: **the closed world is the reason declarations are mandatory** — the builder can only include what it is told about, whether it deduces it or you supply it. ## Edge cases and gotchas - **Constant-folded reflection** — `Class.forName("com.acme.Fixed")` with a *string literal* can sometimes be resolved by the analysis, because the constant is visible. Non-constant strings cannot. - **Conditional reflection** — code paths behind runtime flags still get analysed; the analysis is conservative, but string-driven targets remain opaque. - **Third-party libraries** — a dependency reflecting over *your* classes needs *your* classes registered; the library shipping its own hints doesn't cover your types. ## When it matters Only for native-image targets. On the JVM the open world hides all of this. Because the failure is invisible until you run the native binary, teams add native tests to the pipeline.

  • Can Class.forName ever be resolved by the analysis without a hint?
    Sometimes — if the argument is a compile-time constant string literal, GraalVM's analysis (constant folding) can identify the target class and include it. Non-constant, runtime-computed names cannot be resolved and need explicit registration.
  • Why do native images start faster because of the closed world?
    There's no classpath scanning, no lazy classloading, no JIT warm-up, and no speculative reflection metadata to build — the reachable set is already compiled and the heap can be pre-initialized, so startup is essentially just running compiled code.

saying these in an interview costs you the question

  • Saying native image is just 'the JVM but faster' with no semantic difference
  • Claiming the analysis can trace any Class.forName call
  • Thinking registering the library is enough when it reflects over your own classes

context