Why Java Reflection Is Slow and How It Works Under the Hood
This article explains the internal workings of Java reflection, from obtaining Method objects via getMethod and getDeclaredMethod to the invoke process, and details why reflection incurs significant performance overhead due to argument boxing, visibility checks, parameter validation, lack of inlining, and JIT optimization limits.
Prerequisite Knowledge
Understand basic usage of Java reflection
What You Will Achieve After Reading
Understand the principle of Java reflection and why its performance is low
Article Overview
In Java development we inevitably encounter reflection, and many frameworks rely heavily on it. The common belief is that reflection is slow and should be avoided, but how slow is it really and why? This article explores those questions using OpenJDK 12 source code.
We first present the conclusion, then analyze the principle of Java reflection, allowing readers to reflect on the source code and understand the root causes of its inefficiency.
Conclusion First
Java reflection performance issues stem from:
Method#invoke performs argument boxing and unboxing
Method visibility checks are required
Argument type verification is performed
Reflective methods are hard to inline
JIT cannot optimize reflective calls
1. Principle – Obtaining the Method to Reflect
1.1 Using Reflection
Example code:
<code>public class RefTest {
public static void main(String[] args) {
try {
Class clazz = Class.forName("com.zy.java.RefTest");
Object refTest = clazz.newInstance();
Method method = clazz.getDeclaredMethod("refMethod");
method.invoke(refTest);
} catch (Exception e) {
e.printStackTrace();
}
}
public void refMethod() {
}
}
</code>When invoking reflection we first create a
Classobject, obtain a
Methodobject, then call
invoke. Two ways to obtain a method are
getMethodand
getDeclaredMethod, which we will examine step by step.
1.2 getMethod / getDeclaredMethod
Key parts of the implementation (simplified):
<code>class Class {
@CallerSensitive
public Method getMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
Objects.requireNonNull(name);
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 1. Check method visibility
checkMemberAccess(sm, Member.PUBLIC, Reflection.getCallerClass(), true);
}
// 2. Obtain Method object
Method method = getMethod0(name, parameterTypes);
if (method == null) {
throw new NoSuchMethodException(methodToString(name, parameterTypes));
}
// 3. Return a copy
return getReflectionFactory().copyMethod(method);
}
@CallerSensitive
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
Objects.requireNonNull(name);
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 1. Check method visibility (DECLARED)
checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true);
}
Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
if (method == null) {
throw new NoSuchMethodException(methodToString(name, parameterTypes));
}
// 3. Return a copy
return getReflectionFactory().copyMethod(method);
}
}
</code>Both methods follow a three‑step process: check visibility, obtain the
Methodobject, and return a copy.
The difference lies in the
Memberflag used:
<code>interface Member {
/** All public members, including inherited */
public static final int PUBLIC = 0;
/** All declared members of the class, regardless of visibility, excluding inherited */
public static final int DECLARED = 1;
}
</code> getMethodreturns public members (including inherited), while
getDeclaredMethodreturns all members declared in the class itself.
1.3 getMethod Implementation
Flow diagram (omitted) and source:
<code>class Class {
public Method getMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
Objects.requireNonNull(name);
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkMemberAccess(sm, Member.PUBLIC, Reflection.getCallerClass(), true);
}
Method method = getMethod0(name, parameterTypes);
if (method == null) {
throw new NoSuchMethodException(methodToString(name, parameterTypes));
}
return getReflectionFactory().copyMethod(method);
}
}
</code>The three steps are:
Check method visibility
Obtain the
Methodobject
Return a copy
1.3.1 checkMemberAccess
<code>class Class {
private void checkMemberAccess(SecurityManager sm, int which,
Class<?> caller, boolean checkProxyInterfaces) {
// Default policy allows access to all PUBLIC members and classes with the same class loader.
// Otherwise RuntimePermission("accessDeclaredMembers") is required.
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (which != Member.PUBLIC) {
ClassLoader cl = getClassLoader0();
if (ccl != cl) {
sm.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION);
}
}
this.checkPackageAccess(sm, ccl, checkProxyInterfaces);
}
}
</code>For non‑PUBLIC access an additional security check is performed, requiring the runtime permission
accessDeclaredMembers.
1.3.2 getMethod0
<code>class Class {
private Method getMethod0(String name, Class<?>[] parameterTypes) {
PublicMethods.MethodList res = getMethodsRecursive(
name,
parameterTypes == null ? EMPTY_CLASS_ARRAY : parameterTypes,
true);
return res == null ? null : res.getMostSpecific();
}
}
</code> getMethodsRecursivegathers candidate methods and selects the most specific one based on return type.
1.3.3 getMethodsRecursive
<code>class Class {
private PublicMethods.MethodList getMethodsRecursive(String name,
Class<?>[] parameterTypes,
boolean includeStatic) {
// 1. Get own public methods
Method[] methods = privateGetDeclaredMethods(true);
// 2. Filter matching methods
PublicMethods.MethodList res = PublicMethods.MethodList.filter(methods, name, parameterTypes, includeStatic);
if (res != null) return res;
// 3. Recurse into superclass
Class<?> sc = getSuperclass();
if (sc != null) {
res = sc.getMethodsRecursive(name, parameterTypes, includeStatic);
}
// 4. Search interfaces
for (Class<?> intf : getInterfaces(false)) {
res = PublicMethods.MethodList.merge(res, intf.getMethodsRecursive(name, parameterTypes, false));
}
return res;
}
}
</code>The algorithm checks the class itself, then its superclass, and finally its interfaces.
1.3.4 privateGetDeclaredMethods
<code>class Class {
private Method[] privateGetDeclaredMethods(boolean publicOnly) {
ReflectionData<T> rd = reflectionData();
if (rd != null) {
Method[] res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
if (res != null) return res;
}
// No cache – fetch from JVM
Method[] res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
if (rd != null) {
if (publicOnly) rd.declaredPublicMethods = res; else rd.declaredMethods = res;
}
return res;
}
}
</code>The method first tries a soft‑reference cache (
ReflectionData) and falls back to the native
getDeclaredMethods0if the cache misses.
2. Principle – Invoking a Reflective Method
After obtaining a
Method, the call is performed via
Method#invoke:
<code>class Method {
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (!override) {
Class<?> caller = Reflection.getCallerClass();
// 1. Check access permissions
checkAccess(caller, clazz,
Modifier.isStatic(modifiers) ? null : obj.getClass(),
modifiers);
}
// 2. Get MethodAccessor
MethodAccessor ma = methodAccessor;
if (ma == null) {
ma = acquireMethodAccessor();
}
// 3. Delegate to MethodAccessor.invoke
return ma.invoke(obj, args);
}
}
</code>2.1 Access Check
If
overrideis false (i.e.,
setAccessible(true)has not been called), the JVM verifies that the caller has permission to invoke the target method.
2.2 MethodAccessor Acquisition
<code>class Method {
private MethodAccessor acquireMethodAccessor() {
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}
return tmp;
}
}
</code>The factory creates one of three implementations based on the
noInflationflag:
MethodAccessorImpl– generated Java bytecode (fast after inflation)
NativeMethodAccessorImpl– native implementation (used initially)
DelegatingMethodAccessorImpl– delegates to either of the above
2.3 Invocation Path
<code>class NativeMethodAccessorImpl extends MethodAccessorImpl {
public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException {
if (++numInvocations > ReflectionFactory.inflationThreshold() &&
!ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
// Switch to generated Java version after enough calls
MethodAccessorImpl acc = (MethodAccessorImpl) new MethodAccessorGenerator()
.generateMethod(method.getDeclaringClass(), method.getName(),
method.getParameterTypes(), method.getReturnType(),
method.getExceptionTypes(), method.getModifiers());
parent.setDelegate(acc);
}
// Native call
return invoke0(method, obj, args);
}
private static native Object invoke0(Method m, Object obj, Object[] args);
}
</code>The native accessor counts invocations; after a threshold (default 15) it “inflates” to the generated Java accessor, which is ~20× faster for subsequent calls.
Parameter Checking in Generated Accessor
<code>// Inside MethodAccessorGenerator.emitInvoke (simplified)
for (int i = 0; i < parameterTypes.length; i++) {
if (isPrimitive(paramType)) {
// Unboxing with type checks and possible widening conversions
// If conversion fails, throw IllegalArgumentException
}
}
</code>The generated bytecode validates each argument, performs unboxing, and applies widening where necessary, adding overhead compared to a direct call.
3. Why Java Reflection Is Slow
1. Argument Boxing/Unboxing
Method#invokereceives an
Object[]. Primitive arguments must be boxed (e.g.,
long→
Long) and later unboxed, creating temporary objects and extra memory pressure.
2. Visibility Checks
Every reflective invocation re‑evaluates method accessibility, invoking security manager checks.
3. Parameter Verification
The runtime validates that each supplied argument matches the formal parameter type, adding further cost.
4. Inlining Barriers
Reflective calls are opaque to the JIT; they cannot be inlined, preventing many compiler optimizations.
5. JIT Optimizations Disabled
Because reflection involves types that are dynamically resolved, certain JVM optimizations cannot be performed. Consequently, reflective operations have slower performance than their non‑reflective counterparts and should be avoided in performance‑critical code.
The dynamic nature of reflection prevents the JIT from applying typical optimizations such as escape analysis or method inlining.
Summary
The article dissected the internal implementation of Java reflection, from method lookup to invocation, and identified the main reasons for its poor performance: argument boxing/unboxing, security and visibility checks, parameter validation, inability to inline, and the JIT’s limitation on optimizing reflective calls.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.