-
Notifications
You must be signed in to change notification settings - Fork 0
03 Reflection Internals
Keywords: Reflection, Introspection, java.lang.reflect, Class, Field, Method, Constructor, MethodHandles, VarHandle, invokedynamic, LambdaMetafactory, Dynamic Invocation, Runtime Metadata, Annotation Scanning, Dynamic Proxies, Proxy, InvocationHandler, Framework Metaprogramming, JIT, Inlining, Inflation, NativeMethodAccessor, MethodAccessor, JPMS, Module System, Encapsulation, Security, Unsafe, Bytecode Generation, Dependency Injection, ORM, Serialization, Testing, Performance, Runtime Discovery, Class Loading
Reflection is one of the most powerful features in Java.
It allows code to inspect, discover, and interact with itself at runtime.
That means a program can:
- inspect classes, fields, methods, constructors, and annotations
- create objects dynamically
- invoke methods dynamically
- access metadata about generic signatures and modifiers
- generate proxy objects
- build framework systems that adapt to unknown types
Without reflection, much of modern Java infrastructure would not exist.
Frameworks like:
- Spring
- Hibernate
- Jackson
- JUnit
- Mockito
- CDI
- gRPC tooling
- plugin engines
- serialization systems
all depend heavily on runtime introspection.
But reflection is not free.
It comes with:
- slower access than direct calls
- access checks
- boxing and unboxing overhead
- weaker JIT optimization opportunities
- module encapsulation restrictions
- more complex debugging
So reflection is both:
A Power Tool
and
A Performance and Design Tradeoff
Understanding reflection is the transition from:
Using Frameworks
to:
Building Frameworks
Reflection is the ability of a running Java program to examine and modify its own structure and behavior at runtime.
In Java, this is mainly provided by:
java.lang.Classjava.lang.reflect.Fieldjava.lang.reflect.Methodjava.lang.reflect.Constructorjava.lang.reflect.Parameterjava.lang.reflect.AnnotatedElementjava.lang.reflect.Proxy
At a high level, reflection answers questions like:
- What class is this object?
- What methods does it have?
- What fields are declared?
- What annotations are present?
- Can I instantiate it dynamically?
- Can I invoke a method without compile-time knowledge?
- Can I inspect generic signatures?
This makes reflection a form of:
Runtime Metaprogramming
Java is statically typed.
Normally, the compiler wants to know everything ahead of time:
- class names
- method names
- field types
- constructor signatures
- inheritance relationships
But frameworks often need to work with classes they do not know at compile time.
Examples:
- a dependency injection container discovers beans dynamically
- an ORM maps database rows to unknown entity types
- a JSON library reads object fields without hardcoding model types
- a testing framework invokes methods annotated with
@Test - a plugin system loads classes from external jars
Reflection solves this by allowing code to discover structure at runtime.
The heart of reflection in Java is the Class object.
Every loaded type has a corresponding Class<?>.
Example:
Class<String> c = String.class;Or:
Class<?> c = obj.getClass();Once you have a Class object, you can inspect:
- declared methods
- public methods
- fields
- constructors
- interfaces
- superclass
- annotations
- modifiers
- type parameters
- class loader identity
| Type | Purpose |
|---|---|
Class<?> |
Represents a loaded type |
Field |
Represents a field |
Method |
Represents a method |
Constructor<?> |
Represents a constructor |
Parameter |
Represents a method/constructor parameter |
Annotation |
Represents annotation metadata |
Proxy |
Creates runtime-generated proxy objects |
There are several ways to get class metadata.
Class<String> type = String.class;This is compile-time known and fast.
Class<?> type = obj.getClass();Useful when the exact type is unknown.
Class<?> type = Class.forName("com.example.User");This loads a class by fully qualified name.
Very common in:
- plugin systems
- JDBC
- application frameworks
- dynamic loading
A Class object is not just a type token.
It is a runtime metadata structure containing:
- name
- package
- modifiers
- superclass
- interfaces
- annotations
- methods
- fields
- constructors
- generic signature
- class loader identity
This metadata is used heavily by reflection-based frameworks.
Once you have a Class, you can inspect its structure.
Class<?> clazz = User.class;
System.out.println(clazz.getName());
System.out.println(clazz.getSuperclass());
System.out.println(clazz.getInterfaces().length);| Method | Purpose |
|---|---|
getName() |
Fully qualified class name |
getSimpleName() |
Simple class name |
getPackage() |
Package information |
getSuperclass() |
Parent class |
getInterfaces() |
Implemented interfaces |
getModifiers() |
Access modifiers |
getDeclaredMethods() |
Methods declared in the class |
getDeclaredFields() |
Fields declared in the class |
getDeclaredConstructors() |
Constructors declared in the class |
Reflection lets you inspect and use members dynamically.
Field field = clazz.getDeclaredField("name");Fields represent class state.
You can query:
- name
- type
- modifiers
- annotations
And you can read/write them if allowed.
Method method = clazz.getDeclaredMethod("setName", String.class);Methods represent behavior.
You can query:
- name
- return type
- parameters
- annotations
- modifiers
- exceptions
Constructor<?> ctor = clazz.getDeclaredConstructor(String.class);Constructors are used to instantiate objects dynamically.
Java protects private members.
By default, reflection respects access rules.
Example:
field.setAccessible(true);This disables normal access checks.
But this comes with tradeoffs:
- breaks encapsulation
- may fail under module restrictions
- may be restricted by newer Java versions
- can trigger warnings or exceptions in strongly encapsulated environments
Historically, reflection was permissive. Modern Java is stricter.
Using:
setAccessible(true)means:
Bypass Java Access Checks
This is powerful, but dangerous.
Consequences:
- can access private state
- can violate invariants
- can reduce security
- can break under modules
- can make code fragile
Modern Java modules make deep reflection harder unless packages are opened explicitly.
Example:
Method method = clazz.getDeclaredMethod("sayHello");
method.setAccessible(true);
Object result = method.invoke(obj);This lets you call methods when you do not know them at compile time.
Useful for:
- test frameworks
- dependency injection
- serialization
- scripting integrations
- plugin systems
But Method.invoke() is slower than a direct call.
When you call method.invoke(obj, args...), it is not a simple direct CPU jump. It is a multi-step reflective execution path.
-
Security check
Does the caller have permission to access this method? -
Type checking
Are the arguments valid? This may require boxing, unboxing, and upcasting. -
Delegation
The call is routed through aMethodAccessor.
The JVM uses an adaptive strategy for reflective invocation called inflation.
At first, reflective calls are routed through:
NativeMethodAccessorImpl
This uses JNI/native bridging.
Why?
- quick to start
- avoids immediate bytecode generation
- good for methods called only a few times
But repeated JNI-based reflection is slow.
So after enough invocations, the JVM “inflates” the accessor and generates bytecode-backed accessors.
By default, around the 15th call (configurable via JVM internals such as inflation threshold behavior), the JVM may switch from native reflection to generated bytecode accessors.
Invocation 1..N
Method.invoke()
→ DelegatingMethodAccessor
→ NativeMethodAccessorImpl (JNI path)
After threshold
Method.invoke()
→ DelegatingMethodAccessor
→ GeneratedMethodAccessorXXX (bytecode path)
This reduces repeated JNI overhead.
Even with inflation, Method.invoke() is slower than a direct method call. Why?
-
Boxing and unboxing
invokeusesObject[]arguments. Primitive values must be boxed. -
Type verification
The JVM must validate arguments every call. -
No inlining
The JIT often cannot inline reflective calls because the target is not statically known. -
Metadata lookup
Reflection relies on runtime metadata and access logic.
For hot paths, classic reflection is usually too expensive.
To fix the performance and design flaws of classic reflection, Java 7 introduced the java.lang.invoke package.
A MethodHandle is a strongly-typed, directly executable reference to an underlying method, constructor, or field.
| Feature | java.lang.reflect.Method |
java.lang.invoke.MethodHandle |
|---|---|---|
| Access checks | Every call path may pay overhead | Checked during lookup |
| Type safety | Weak (Object[]) |
Strong (invokeExact requires exact match) |
| JIT optimization | Poor | Excellent |
| Speed | Slower | Near-native performance |
| Framework suitability | Legacy reflection | Modern runtime linkage |
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(String.class);
MethodHandle handle = lookup.findVirtual(MyClass.class, "myMethod", type);
MyClass instance = new MyClass();
String result = (String) handle.invokeExact(instance);MethodHandles are fast, type-aware, and more JIT-friendly than reflection.
MethodHandles paved the way for the most important bytecode instruction added to Java since 1.0: invokedynamic.
Before invokedynamic, bytecode instructions for method calls were hardcoded to exact classes.
invokedynamic allows the JVM to defer method linking until runtime.
When the JVM reaches an invokedynamic instruction, it calls a bootstrap method that wires up a MethodHandle dynamically.
Why does this matter?
- it powers Java lambdas
- it enables dynamic languages on the JVM
- it allows runtime linkage strategies that are much more flexible than classic bytecode call sites
Java lambdas are not implemented as old-style anonymous inner classes in the traditional sense anymore; they are linked through invokedynamic and LambdaMetafactory.
Java supports runtime-generated proxy objects.
Dynamic proxies allow code to implement interfaces at runtime and intercept method calls.
Typical use cases:
- AOP
- logging
- transaction wrapping
- remote method invocation
- interception
- lazy loading
- method decoration
[ Client Code ]
│
▼
[ Generated Proxy Class ]
│
▼
[ InvocationHandler.invoke() ]
│
├──▶ [ Actual Target Object ]
│
▼
[ Returned Result ]
Classic java.lang.reflect.Proxy can only proxy interfaces.
If you need to proxy a concrete class, frameworks usually fall back to bytecode generation libraries such as:
- CGLIB
- Byte Buddy
- ASM-based generation
Frameworks use reflection to avoid hardcoding application types.
Example patterns:
- scan classpath
- inspect annotations
- create instances
- inject dependencies
- invoke lifecycle methods
- map external data to objects
- wrap behavior with proxies
This gives frameworks flexibility, but also introduces overhead and complexity.
Annotations are a major reason reflection is so important.
Reflection allows code to inspect annotations on:
- classes
- methods
- fields
- parameters
- constructors
- packages
Example:
if (method.isAnnotationPresent(Test.class)) {
// run test
}Annotations power:
- Spring annotations
- JUnit annotations
- validation frameworks
- mapping frameworks
- transaction systems
Reflection can inspect parameter names and types.
Example:
Parameter[] params = method.getParameters();This is useful for:
- validation
- dependency resolution
- API documentation generation
- code generation tools
Parameter metadata can be critical for framework design.
Java generics are erased at runtime, but much metadata is still available.
Examples:
getGenericSuperclass()getGenericInterfaces()getGenericReturnType()getGenericParameterTypes()
This allows frameworks to inspect:
- generic collections
- type hierarchies
- parameterized types
- wildcard types
- type variables
However, due to type erasure, runtime generic information is partial and must often be recovered from signatures and declarations.
Generics in Java are mostly compile-time constructs.
At runtime:
List<String> and List<Integer>
both become roughly:
List
Reflection can still recover certain generic signatures, but not actual runtime element types inside collections unless those types are encoded in metadata or inferred by convention.
This is why frameworks often use:
- type tokens
- generic superclasses
- annotation metadata
- custom serializers
Reflection is slower than direct code for several reasons:
- access checks
- indirection
- boxing and unboxing
- lack of easy inlining
- metadata lookup
- exception-heavy APIs
- less predictable optimization
In hot paths, raw reflection can become a bottleneck.
The JIT compiler likes:
- concrete call targets
- stable types
- inlineable methods
- predictable control flow
Reflection can make code more dynamic and opaque.
That means:
- less inlining
- fewer optimizations
- more runtime checks
- lower peak throughput
For performance-sensitive code, prefer alternatives when possible.
Modern Java provides runtime tools that are faster and more structured than classic reflection.
Introduced in Java 7, MethodHandles provide a faster, more structured way to perform dynamic invocation.
Package:
java.lang.invokeAdvantages:
- faster
- more JIT-friendly
- type-aware
- suitable for advanced runtime systems
- used heavily by modern frameworks
Where reflection says:
I can invoke this method by name.
MethodHandles say:
I can dynamically link this method in a JVM-friendly way.
VarHandle is another modern runtime API in java.lang.invoke.
It provides controlled access to fields and array elements with:
- atomic operations
- memory ordering control
- fence-aware access
It is a safer and more modern low-level tool than Unsafe for many use cases.
sun.misc.Unsafe and its successors expose extremely low-level operations:
- raw memory access
- CAS
- direct field offsets
- off-heap allocation
- fences
This is powerful, but dangerous.
It bypasses many safety guarantees and can destabilize the JVM if misused.
Modern Java modules introduce stronger encapsulation.
This means deep reflection may be restricted unless:
- packages are opened
- modules are explicitly configured
- runtime access is allowed
This is a major shift from older Java versions where reflective access was easier.
The module system improves safety and encapsulation, but it makes some reflection-heavy legacy patterns more fragile.
If you attempt deep reflective access on a closed module, Java may throw an InaccessibleObjectException.
Reflection can bypass encapsulation, which is why it has security implications.
Potential risks:
- accessing private state
- mutating invariants
- bypassing intended API restrictions
- exposing sensitive internals
That is why reflective access must be used deliberately and carefully.
Historically, SecurityManager helped regulate some of this behavior, but it is largely legacy now; modern Java relies more on module boundaries and strong encapsulation.
Reflection is foundational for:
- dependency injection
- ORM mapping
- serialization/deserialization
- test discovery
- plugin loading
- annotation processing
- API introspection
- runtime code generation
- dynamic configuration systems
Without reflection, many Java frameworks would lose their flexibility.
Reflection is often too slow for performance-critical inner loops.
Modern Java may block deep reflection unless the module system permits it.
This can make code brittle and harder to maintain.
Type erasure limits what reflection can reconstruct.
Reflection often throws checked exceptions and requires careful handling.
It is still essential for many frameworks, but it should be used with clear intent and measured carefully.
| Aspect | Direct Code | Reflection |
|---|---|---|
| Speed | Fast | Slower |
| Type Safety | Strong | Weaker |
| Flexibility | Lower | Higher |
| JIT Friendliness | High | Lower |
| Framework Use | Limited | Massive |
| Debuggability | Easier | Harder |
The right choice depends on the context.
High-performance systems often combine reflection with caching.
Strategies include:
- cache
MethodandFieldobjects - avoid repeated lookups
- use
MethodHandleswhen possible - precompute metadata at startup
- move reflection out of hot loops
This is how modern frameworks reduce reflective overhead.
A common framework design pattern is:
Scan
→ Inspect
→ Cache Metadata
→ Build Execution Plan
→ Invoke Efficiently
This allows a framework to use reflection at startup or initialization time, then switch to cached, optimized execution paths afterward.
Reflection is not the only way to do runtime metaprogramming.
Modern alternatives include:
MethodHandlesVarHandleinvokedynamic- bytecode generation libraries
- annotation processors
- compile-time code generation
Examples:
- MapStruct
- Micronaut
- Byte Buddy
- ASM
Compile-time code generation often avoids runtime reflection overhead and can improve GraalVM compatibility.
Reflection remains essential, but newer Java features are shaping how it is used:
- modules encourage stronger encapsulation
-
MethodHandlesimprove performance -
VarHandleoffers safer low-level access - records simplify metadata-driven code
- sealed classes help model domain hierarchies more predictably
Reflection is no longer the only runtime metaprogramming tool, but it is still foundational.
Ahead-of-Time (AOT) compilers like GraalVM Native Image are the enemy of uncontrolled reflection.
AOT compilation requires a “closed world” assumption. The compiler analyzes reachable code at build time and strips out unused code aggressively.
Because reflection is dynamic:
Class<?> clazz = Class.forName(readLineFromUser());the compiler cannot always know which class will be loaded.
The fix is to provide explicit metadata configuration, such as reflection configuration files, so the AOT compiler knows which classes, methods, and fields may be accessed reflectively.
Tradeoff:
- less runtime flexibility
- better startup
- smaller binaries
- more configuration discipline
Useful tools for analyzing reflective systems:
| Tool | Purpose |
|---|---|
| JFR | Profile reflective call overhead |
| JMH | Benchmark reflection vs MethodHandles |
jstack |
See thread behavior under reflective frameworks |
jcmd |
JVM diagnostics |
| Byte Buddy / ASM | Bytecode generation and runtime instrumentation |
Continue exploring:
- 03-Runtime-Overview
- 03-ClassLoader-Architecture
- 02-Java-Memory-Model
- 02-ExecutorService-Internals
- 04-Performance-Overview
- 04-Event-Loop-Design
Reflection is the point where Java stops being only a statically typed language and becomes a runtime-adaptive platform.
It lets code inspect itself, load unknown components, wire systems dynamically, and build sophisticated frameworks. But it also introduces overhead, complexity, and encapsulation tradeoffs.