One of the most common mistakes Java developers make when adopting GraalVM Native Images is assuming that reflection will “just work” the same way it does on the JVM. After all, reflection powers frameworks like Spring, Hibernate, Jackson, and JUnit—so it’s natural to expect it in native images. The surprise comes when code that relies on dynamic class loading, proxies, or annotation scanning fails at runtime because GraalVM eliminates unused metadata during ahead-of-time (AOT) compilation.
Reflection in standard JVMs works because class metadata is loaded and available at runtime. In GraalVM native images, however, metadata is aggressively pruned to achieve faster startup and smaller binaries. This means you must explicitly configure reflection usage.
Think of it like packing for a trip: on the JVM, you bring your entire wardrobe (all class metadata). On GraalVM, you only pack the clothes you say you’ll wear (explicit reflection config). If you forget to pack something, you’re stuck without it.
Why Reflection is Limited in GraalVM
- Closed-World Assumption – GraalVM assumes the complete application is known at build time.
- Metadata Elimination – Class metadata not referenced directly is removed.
- Dynamic Class Loading –
Class.forName()
ornewInstance()
may fail without explicit registration. - Proxies – JDK dynamic proxies and CGLIB proxies need ahead-of-time configuration.
Example: Reflection Failure in GraalVM
Class<?> clazz = Class.forName("com.example.User");
Object obj = clazz.getDeclaredConstructor().newInstance();
On a JVM: works fine.
On a GraalVM native image: throws ClassNotFoundException
unless com.example.User
is registered for reflection.
Workarounds and Solutions
1. Reflection Configuration JSON
You can register classes explicitly with a reflect-config.json
file:
[{
"name" : "com.example.User",
"allDeclaredConstructors" : true,
"allDeclaredFields" : true,
"allDeclaredMethods" : true
}]
Build with:
native-image --reflection-config=reflect-config.json -jar app.jar
2. Automatic Configuration with native-image-agent
Run the app on JVM with the agent:
java -agentlib:native-image-agent=config-output-dir=./configs -jar app.jar
This generates configs for reflection, proxies, and resources based on runtime usage.
3. Using Substitutions
If reflection is unavoidable, GraalVM allows substitutions to replace unsupported APIs with custom implementations.
@TargetClass(className = "java.lang.ClassLoader")
final class Target_ClassLoader {
@Substitute
private Class<?> defineClass(...) {
throw new UnsupportedOperationException("defineClass not supported in native image");
}
}
4. Framework Support
Modern frameworks adapt automatically:
- Spring Native – Replaces heavy reflection with AOT hints.
- Micronaut – Designed from the ground up with reflection-free DI.
- Quarkus – Uses build-time processing and GraalVM-friendly substitutions.
Pitfalls of Reflection in GraalVM
- Unregistered Classes – Leads to runtime errors.
- Slower Builds – Overly broad reflection configs increase image size.
- Proxies Not Configured – Breaks AOP or JDK proxy usage.
- Serialization Issues – Jackson/Hibernate often need reflection hints.
- Debugging Complexity – Errors only appear at runtime in native image.
📌 What's New in Java Versions?
- Java 5: Introduced annotations, which often require reflection.
- Java 8: Lambdas and streams increased reflective proxy usage.
- Java 9: Module system limited deep reflection.
- Java 11: LTS version heavily used with GraalVM.
- Java 17: Sealed classes require explicit reflection registration.
- Java 21: GraalVM native image officially integrated into OpenJDK via Project Leyden efforts.
Real-World Analogy
Running reflection-heavy frameworks on the JVM is like ordering from an all-you-can-eat buffet—everything is available at runtime. GraalVM Native Image is like a pre-packed lunchbox—you only get what you specify in advance. If you forget to include dessert (reflection config), you won’t have it at runtime.
Best Practices
- Avoid deep reflection in hot paths—prefer MethodHandles or direct calls.
- Use frameworks designed for GraalVM (Micronaut, Quarkus, Spring Native).
- Generate reflection configs with
native-image-agent
instead of writing manually. - Keep configs minimal—register only what’s required.
- Benchmark startup and memory usage after adding reflection.
- Follow GraalVM release notes for evolving reflection support.
Summary + Key Takeaways
- Reflection is limited in GraalVM due to closed-world and AOT compilation.
- You must register reflective access explicitly using JSON configs or agents.
- Frameworks like Spring Native, Micronaut, and Quarkus handle this automatically.
- Pitfalls include missing configs, larger images, and debugging challenges.
- Best practices reduce risk and ensure production-ready native images.
FAQs
Q1. Why does reflection fail in GraalVM native images?
Because GraalVM prunes class metadata unless explicitly registered.
Q2. How do I fix ClassNotFoundException
in a native image?
Register the class in reflect-config.json
or run with native-image-agent
.
Q3. Does GraalVM completely disable reflection?
No, it requires explicit configuration for reflective access.
Q4. How do frameworks like Spring handle reflection in GraalVM?
Spring Native provides AOT hints and substitutions for reflective APIs.
Q5. Can I still use Jackson/Hibernate with GraalVM?
Yes, but they require reflection configs for entity/DTO classes.
Q6. Does reflection impact native image size?
Yes, registering too much metadata increases binary size.
Q7. Is there a way to eliminate reflection entirely?
Yes—use frameworks like Micronaut or Quarkus that avoid reflection with AOT code generation.
Q8. Can I debug missing reflection configs?
Yes, run with --verbose
or inspect logs for missing class metadata.
Q9. Are proxies supported in GraalVM?
Yes, but JDK dynamic proxies must be pre-registered in proxy-config.json
.
Q10. Does GraalVM support runtime class generation like CGLIB?
No, dynamic bytecode generation is not supported; use build-time alternatives.
Q11. Is GraalVM reflection support improving?
Yes—newer releases (Java 21+) integrate native image tooling more tightly into OpenJDK.