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:

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;
}
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:

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();
    }
}
getMethodsRecursive

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

receives an Object[]. Primitive arguments must be boxed (e.g., longLong) 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javaReflectionJITJDKMethod 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

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.