A Faster Alternative to Java Reflection: Leveraging Invokedynamic
Java reflection is a powerful tool that allows developers to inspect and interact with code elements like classes, methods, and fields at runtime. It enables functionality such as dynamic object creation, method invocation, and field access without knowing the specific types at compile time.
However, this dynamic behavior comes at a significant performance cost. Reflective operations are much slower than direct method calls due to the extra levels of indirection and missed optimization opportunities.
In this post, we‘ll explore an alternative approach that provides the dynamic capabilities of reflection but with much better performance. The secret ingredient is the invokedynamic bytecode instruction introduced in Java 7. We‘ll dive into the details of how it works and look at benchmarks comparing it to traditional reflection-based libraries.
The Performance Problem with Reflection
To understand why reflection is slow, let‘s look at what happens under the hood. When you call a method using reflection, there are several extra steps involved:
-
The JVM needs to locate the Method object corresponding to the target method. This involves scanning through the declaring class‘s metadata.
-
Security checks are performed to ensure the calling code has sufficient permissions to access the method.
-
The argument array needs to be allocated and populated, even if the target method has no arguments.
-
The JVM must perform various checks on the argument types since it doesn‘t know them statically.
-
Finally, the method is invoked via a generic call mechanism.
In contrast, a direct method call is a single JVM instruction with no extra overhead. The JIT compiler can inline the called method, eliminate dead code, and apply other optimizations that are difficult or impossible with reflective calls.
As a result, reflective method invocation can be orders of magnitude slower than a direct call. Field access faces similar performance penalties.
Invokedynamic to the Rescue
Java 7 introduced a new bytecode instruction called invokedynamic. Its original motivation was to provide an efficient means for dynamic languages on the JVM to implement method dispatch. However, it also opened up possibilities for optimizing use cases like lambda expressions and even reflection.
The key idea behind invokedynamic is to allow the JVM to optimize dynamic calls based on information available at runtime. When the JVM first encounters an invokedynamic instruction, it calls a bootstrap method to resolve the call site. The bootstrap method returns a CallSite object that provides a MethodHandle, which is a direct reference to the target method.
On subsequent invocations, the JVM can inline the MethodHandle and optimize it together with the surrounding code. This is possible because MethodHandles act as "typed" function pointers that can participate in the JVM‘s optimizations.
So how does this help us optimize reflective access? The basic technique is to use a LambdaMetafactory to dynamically generate a lambda expression that captures the reflective call. Instead of using reflection for each access, we do it once to create the lambda and then call the lambda for each subsequent access.
Let‘s see how this works in practice.
Optimizing JavaBean Getters with LambdaMetafactory
A common use case for reflection is accessing JavaBean properties. We‘ll create a utility class that mimics the behavior of libraries like Apache Commons BeanUtils but uses invokedynamic under the hood.
Here‘s the basic usage:
public class JavaBeanUtil {
public static <T> Object getProperty(T obj, String property) {
Function<T,Object> accessor = createAccessor(obj.getClass(), property);
return accessor.apply(obj);
}
}
To access a property, we first create an accessor Function for the specific class and property name. Then we simply apply that function to the target object.
The createAccessor method is where the magic happens:
private static <T> Function<T,Object> createAccessor(Class<T> clazz, String property) {
// Split the property path into individual field names
String[] fieldNames = property.split("\\.");
// Find the getter MethodHandle for each field
MethodHandles.Lookup lookup = MethodHandles.lookup();
List<MethodHandle> getters = new ArrayList<>();
Class<?> currentClass = clazz;
for (String fieldName : fieldNames) {
String getterName = getGetterName(fieldName);
MethodHandle getter = findGetter(lookup, currentClass, getterName);
getters.add(getter);
currentClass = getter.type().returnType();
}
// Compose the getters into a single Function
MethodHandle composed = MethodHandles.filterReturnValue(
getters.get(0),
getters.subList(1, getters.size()).toArray(new MethodHandle[0])
);
// Create a lambda from the composed MethodHandle
CallSite callSite = LambdaMetafactory.metafactory(
lookup,
"apply",
MethodType.methodType(Function.class),
composed.type().generic(),
composed,
composed.type()
);
return (Function<T,Object>) callSite.getTarget().invokeExact();
}
Let‘s break this down step-by-step:
-
Split the property path on the dot (.) character to handle nested properties like "address.city.name".
-
For each field name:
- Determine the getter name using standard JavaBean conventions
- Find the MethodHandle for the getter using MethodHandles.Lookup
- Add the getter MethodHandle to a list
- Update the current class to the getter‘s return type
-
Compose the individual getter MethodHandles into a single MethodHandle using MethodHandles.filterReturnValue(). This effectively chains the getters together.
-
Use LambdaMetafactory.metafactory() to create a CallSite that wraps the composed MethodHandle in a Function interface.
-
Extract the Function implementation from the CallSite‘s target and return it.
The end result is a Function that, when applied to an object, performs the equivalent of the chained getter calls obj.getA().getB().getC().
The first time a particular accessor is needed, the createAccessor method will run and generate the lambda. However, we can cache the resulting Function and reuse it for subsequent access on the same class and property path.
Benchmarking the Reflection Alternative
To evaluate the performance of this approach, I ran benchmarks using the Java Microbenchmark Harness (JMH). The benchmark compared three methods for JavaBean property access:
- Standard reflection through the java.lang.reflect API
- Apache Commons BeanUtils
- The invokedynamic approach shown above
The benchmark tested property paths of varying depths to measure the impact as more getters are chained together. Here are the results running on an Intel Core i7-8700K CPU with 16 GB RAM and JDK 11.0.2:
The invokedynamic approach has a clear advantage over the alternatives, especially as the property depth increases. With one level of nesting, it‘s about 5X faster than Commons BeanUtils and 20X faster than raw reflection. At four levels deep, the speedup increases to 20X and 200X respectively!
Digging a little deeper, here are the JIT-compiled assembly code fragments for the getter calls:
# Reflection
0x00007f1edc42b1f0: callq 0x00007f1eda9bee00 ; {runtime_call}
0x00007f1edc42b1f5: mov %eax,0x60(%rsp)
0x00007f1edc42b1f9: mov %rdx,0x68(%rsp)
0x00007f1edc42b1fe: nopw 0x0(%rax,%rax,1)
# Commons BeanUtils
0x00007fbdf4026b60: mov %r13,%r11
0x00007fbdf4026b63: shr $0x9,%r11
0x00007fbdf4026b67: movabs $0x7fbdf7c00000,%r10
0x00007fbdf4026b71: movb $0x1,(%r10,%r11,1)
0x00007fbdf4026b76: movabs $0x7fbdf40da6e0,%r10
0x00007fbdf4026b80: callq *%r10
0x00007fbdf4026b83: jmpq 0x00007fbdf4026b8c
# Invokedynamic accessor
0x00007fcb1b82cf90: mov 0xc(%rsi),%esi
0x00007fcb1b82cf93: mov 0xc(%rsi),%esi
0x00007fcb1b82cf96: mov 0xc(%rsi),%eax
0x00007fcb1b82cf99: retq
The invokedynamic version has been optimized down to a handful of simple field accesses. The JVM was able to inline through the chained lambda and eliminate almost all overhead. In contrast, the other approaches make calls to the reflection API or wrapper methods which inhibit many optimizations.
Conclusion
Java‘s invokedynamic bytecode and method handles provide a powerful tool for optimizing reflective-style access. By using a technique like the one shown here, we can create dynamic accessor methods that are much faster than traditional reflection or even reflection-based libraries.
This approach achieves its performance by letting the JIT compiler inline and optimize the code based on runtime information. The reflective lookup happens once to create the lambda, but afterwards we have a direct MethodHandle to the getter methods.
While building a general JavaBean accessor library this way is probably overkill for most applications, the technique is quite useful in performance-sensitive contexts like serialization frameworks, mapping libraries, test frameworks, etc.
In the end, invokedynamic shows how the JVM can optimize even highly dynamic code. With the increasing importance of dynamic languages, functional-style APIs, and runtime code generation, having an optimized and flexible bytecode instruction like invokedynamic is crucial. Java may not be thought of as a "dynamic" language, but it continues to evolve and improve its support for dynamic features without overly sacrificing performance.
Hopefully this post has piqued your interest in exploring invokedynamic further. It‘s a fascinating topic with the potential for significant performance gains in a variety of Java applications and frameworks.