What is the 'closed-world assumption' in GraalVM native image, and how does it explain reflection failures?
answer
- open world (JVM) vs closed world (native)
- reachability closure computed at build
- frozen binary, no runtime discovery
- two failures: class absent vs metadata stripped
- declarations re-add to closed set
basics
~20 sNative 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 sThe 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
Knows the phrase 'closed world' and that it freezes code at build time.
Explains reachability closure and both failure modes (class absent vs metadata stripped).
Notes constant-folding exceptions and that third-party reflection over your types still needs your registration.
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