Why is a reflective Method.invoke slower than a direct call, and how do you cut that cost?
Every invoke pays what a direct call does not: an access check, boxing of each primitive argument into an Object[] and of the return value, and a call site the JIT treats as opaque — so it is not inlined, and the optimizations that ride on inlining are lost. Member lookup also copies Method objects. Cut the cost by caching the Method, calling setAccessible(true), or moving to MethodHandle/LambdaMetafactory, which the JIT can inline.
- ✗Blaming the whole cost on
setAccessible, ignoring boxing and lost inlining - ✗Calling
getDeclaredMethodinside the hot loop instead of caching theMethod - ✗Assuming the JIT eventually optimizes a reflective call into a direct one
- →Why can a
MethodHandlebound to astatic finalfield be inlined whileMethod.invokecannot? - →How does a serializer amortize reflection cost when it maps thousands of objects?
Where the cost comes from
// ❌ member lookup inside the hot loop
for (Object row : rows) {
Method m = row.getClass().getDeclaredMethod("getId"); // copies a Method on every call
ids.add((Long) m.invoke(row)); // boxes the result into a Long
}
// ✅ look up once, then only invoke
private static final Method GET_ID = init(); // cached
static Method init() throws Exception {
Method m = Row.class.getDeclaredMethod("getId");
m.setAccessible(true); // the access check is paid once
return m;
}
1. Lookup. getDeclaredMethod / getDeclaredFields return copies of the Method and Field objects (so a caller cannot corrupt the Class's internal state). In a loop that is an allocation per iteration. Caching removes it.
2. Boxing. The invoke(Object obj, Object... args) signature forces the arguments into an Object[], each primitive into a wrapper. The result comes back as an Object too. A direct call makes none of these allocations.
3. Inlining. This is the expensive, invisible one. The JIT cannot see which method will be called, so the call site is not inlined — and with inlining go devirtualization and escape analysis of the surrounding code. On a hot path the gap against a direct call is an order of magnitude, not a few percent.
What to use instead. A MethodHandle resolved once and stored in a static final field is inlined by the JIT like an ordinary call. LambdaMetafactory goes further: it spins up a real implementation of a functional interface, and subsequent calls are indistinguishable from calling a lambda. That is how fast serializers and mappers work — reflection is paid once at startup, not once per record.