Reflection: Internals, Performance, and Source Analysis
Java is a statically typed language, but Reflection grants it the runtime flexibility usually reserved for dynamic languages. Modern frameworks like Spring, MyBatis, and Hibernate are almost entirely built upon this pillar.
What happens under the hood when you call Class.forName() or method.invoke()? This article peels back the API to explore the Heap, Metaspace, and OpenJDK source code to reveal the true nature of reflection.
1. The X-Ray Vision: What is Reflection?
In short, Reflection is the ability of a Java program to dynamically perceive and manipulate class metadata at runtime.
Without reflection, your code is "hard-wired" at compile-time:
Person p = new Person();
p.sayHello(); // Types and methods must be known to the compiler
Frameworks, however, often don't know the names of the classes they will handle (e.g., Person vs. Order) until the app is running. Reflection allows a framework to read a string from a config file (e.g., "com.example.Person") and instantiate it dynamically.
2. A Tale of Two Worlds: Class Object vs. Klass Model
To understand reflection, we must distinguish between how classes exist in the JVM's C++ core and how they appear to Java code.
2.1 The C++ World: InstanceKlass (Metaspace)
The JVM is written in C++. Internally, it uses a structure called InstanceKlass to store the "Real Metadata"—the constant pool, vtables, and field layouts.
- Why Metaspace?:
InstanceKlassis high-frequency infrastructure for the execution engine. If it were in the Java Heap, the GC's "moving" behavior (memory compaction) would constantly invalidate the stable C++ pointers required for JIT and method dispatch. Thus, since JDK 8, this data lives in Metaspace (Native Memory), managed directly by the OS.
2.2 The Java World: java.lang.Class (The Heap)
Java code cannot touch C++ pointers directly. Instead, when a class is loaded:
- JVM creates an
InstanceKlassin Metaspace (The Blueprint). - JVM creates a
java.lang.Classinstance in the Heap (The Brochure).
This Class object acts as a Mirror. When you call clazz.getMethods(), the Class object uses an internal "secret passage" to fetch the real data from the InstanceKlass in Metaspace.
3. Source Analysis: ReflectionData and Caching
Fetching metadata from the C++ world is expensive. If getDeclaredMethods() parsed the Metaspace every time, performance would collapse. To solve this, java.lang.Class uses a deep caching mechanism: ReflectionData.
public final class Class<T> {
// Cache reflection data via SoftReference
private transient volatile SoftReference<ReflectionData<T>> reflectionData;
private static class ReflectionData<T> {
volatile Field[] declaredFields;
volatile Method[] declaredMethods;
// ...
final int redefinedCount;
}
}
3.1 Why SoftReference?
Reflective metadata is used heavily during startup (e.g., Spring bean injection) but infrequently afterward. SoftReference allows the GC to safely reclaim this memory if the JVM approaches an OutOfMemoryError. If more reflection is needed later, the JVM simply re-fetches the data from Metaspace.
3.2 Global Consistency: redefinedCount
Tooling like Arthas or JRebel can hot-swap bytecode at runtime. If the "Blueprint" changes, the "Brochure" (cache) becomes obsolete. The redefinedCount acts as a version counter; if the class is redefined, the count increments, the cache is invalidated, and a fresh read is triggered.
4. Method.invoke() and the Magic of "Inflation"
Calling method.invoke() doesn't immediately jump to the target code. It follows a fascinating evolutionary path known as Reflection Inflation.
4.1 The Initiation: Native Path
Initially, Method.invoke() delegates to a NativeMethodAccessorImpl. This uses JNI (Java Native Interface) to cross into the C++ world, find the target pointer, and execute it.
- Pros: Zero startup delay.
- Cons: Massive overhead for crossing the JNI boundary and lack of JIT optimization.
4.2 The Threshold: 15 Calls
The JVM tracks the number of invocations via numInvocations. When it reaches the InflationThreshold (default: 15), the JVM changes its strategy.
It decides that JNI is too slow for this highly-active call. It launches MethodAccessorGenerator, which dynamically generates a custom Java class at runtime.
This new class (e.g., GeneratedMethodAccessor1) contains pure Java bytecode that performs a standard cast and calls the target method using invokevirtual. The delegation is then switched from the Native path to this new, high-speed bytecode path.
Analogy: The first 15 times you need to read a French legal document, you pay a "Native Translator" (JNI) to do it. On the 16th time, the translator writes a custom "French-to-English Dictionary" just for that document and gives it to you. Now, you can "read" it yourself at full speed.
Note: Since Java 18 (JEP 416), this legacy mechanism has been replaced by MethodHandles, but the core principle of evolving from dynamic dispatch to JIT-optimizable forms remains identical.
5. Why is Reflection "Slow"?
Now we can see the specific technical reasons for the "Reflection Penalty":
- Search Cost: Standard calls are hard-coded in the Constant Pool. Reflective calls require walking the
ReflectionDataarray and performing string comparisons to find the method name. - Security Checks: Every
invoke()triggers permission checks. CallingsetAccessible(true)improves performance slightly by bypassing these checks. - Autoboxing Overhead: Reflective arguments are passed as
Object[]. Any primitiveintmust be boxed into anIntegeron the heap, increasing GC pressure. - JIT Barrier: JIT compilers (like C2) excel at "Method Inlining." Because reflection is inherently dynamic and uses generic
Objectframes, it's nearly impossible for the compiler to inline the code, losing out on massive performance gains.
Summary
- Archetype & Shadow: Reflection starts with the
InstanceKlassin Metaspace and manifests as thejava.lang.Classobject in the Heap. - Strategic Caching: Reflection uses SoftReferences to balance the high cost of metadata fetching with heap memory constraints.
- Adaptive Evolution: The 15-call "Inflation" threshold demonstrates the JVM's ability to trade initial speed for long-term execution efficiency.