Backend Development 24 min read

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.

macrozheng
macrozheng
macrozheng
Why Java Reflection Is Slow and How It Works Under the Hood

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

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):

<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

Method

object, and return a copy.

The difference lies in the

Member

flag 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>
getMethod

returns public members (including inherited), while

getDeclaredMethod

returns 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

Method

object

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>
getMethodsRecursive

gathers 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

getDeclaredMethods0

if 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

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

<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

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

<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#invoke

receives 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.

JavaperformanceReflectionJITJDKMethod Invocation
macrozheng
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.