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:
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() {
}
}When invoking reflection we first create a Class object, obtain a Method object, then call invoke. Two ways to obtain a method are getMethod and getDeclaredMethod, which we will examine step by step.
1.2 getMethod / getDeclaredMethod
Key parts of the implementation (simplified):
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);
}
}Both methods follow a three‑step process: check visibility, obtain the Method object, and return a copy.
The difference lies in the Member flag used:
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;
} getMethodreturns public members (including inherited), while getDeclaredMethod returns all members declared in the class itself.
1.3 getMethod Implementation
Flow diagram (omitted) and source:
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);
}
}The three steps are:
Check method visibility
Obtain the Method object
Return a copy
1.3.1 checkMemberAccess
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);
}
}For non‑PUBLIC access an additional security check is performed, requiring the runtime permission accessDeclaredMembers.
1.3.2 getMethod0
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();
}
} getMethodsRecursivegathers candidate methods and selects the most specific one based on return type.
1.3.3 getMethodsRecursive
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;
}
}The algorithm checks the class itself, then its superclass, and finally its interfaces.
1.3.4 privateGetDeclaredMethods
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;
}
}The method first tries a soft‑reference cache ( ReflectionData) and falls back to the native getDeclaredMethods0 if the cache misses.
2. Principle – Invoking a Reflective Method
After obtaining a Method, the call is performed via Method#invoke:
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);
}
}2.1 Access Check
If override is 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
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;
}
}The factory creates one of three implementations based on the noInflation flag: MethodAccessorImpl – generated Java bytecode (fast after inflation) NativeMethodAccessorImpl – native implementation (used initially) DelegatingMethodAccessorImpl – delegates to either of the above
2.3 Invocation Path
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);
}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
// 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
}
}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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
