With the introduction of method handles in Java 7 and LambdaMetafactory in Java 8 we can now make reflective method calls almost as fast as the direct access itself.
The Reflections API has been with us since the earliest days of Java. The Class
class was available right from the
start but it wasn't all that useful. JDK 1.1 introduced listing of all members defined within a particular class and a
way to indirectly invoke methods and to modify values of class fields. It was now possible to develop frameworks like
Spring and Hibernate enriching the existing code thanks to metaprogramming features. While these solutions were useful
they weren't particularly convenient due to the XML based configuration.
With JDK 1.5 came the next important piece of the puzzle - annotations. This was the moment to say goodbye to XML and to place configuration right next to the code itself. Instead of specifying the fully qualified name of a class or a method we could finally just mark the target element with all necessary metadata. While annotations don't improve performance they certainly do make things a lot easier.
Reflective access boosts software development efficiency a great deal but there's a price we have to pay. Java
compilation process is great at optimizing direct access code. It can perform dead code elimination, method inlining and
machine code recompilation based on runtime statistics. All of that is off the table for pieces of code using the
java.lang.reflect
package.
Score | Error | |
---|---|---|
Direct access | 3.469 | 0.196 |
Reflections | 6.505 | 0.620 |
Score | Error | |
---|---|---|
Direct access | 3.476 | 0.329 |
Reflections | 5.786 | 0.160 |
Score represents the average time in nanoseconds per single operation. Error uses the same unit.
Method.invoke()
on OpenJDK 8 is 87% slower than direct access. On OpenJDK 11 performance has been improved a bit and
the gap has been decreased to 66%. There is some progress but it will never be as fast as the normal code.
All benchmark data in this article has been generated using Java Microbenchmark Harness by calling in various ways the same getter method. Full code is available on Github. Each test was run 10 times in total with each run lasting for 5 seconds.
Method handles
In JDK 7 a new bytecode instruction invokedynamic
(also known as indy
) has been introduced together with the
MethodHandle
class and its subtypes to represent the linked behaviour. Java developers might not have noticed right
away any change as this new instruction was not used directly by Java just yet. It was only the first step towards
lambda expression support. However, these two elements were crucial for dynamic languages like Groovy.
You can think of MethodHandle
as a new, enriched alternative to the standard Method
class. It can represent any
method including the static ones, field getters and setters and constructors.
When JVM encounters the invokedynamic
instruction in bytecode it first executes a special bootstrap method which
returns a CallSite
object containing the target MethodHandle
. Using that bootstrap method dynamic languages can
determine the final behaviour to be executed based on the method name and types of the provided parameters.
While we can't directly use indy
in Java code we can easily transform Method
instances into MethodHandle
ones.
Method reflectionMethod = Object.class.getMethod("toString");
MethodHandle handle = MethodHandles.lookup().unreflect(reflectionMethod);
Let's see how the new solution fares performance-wise.
Score | Error | |
---|---|---|
Direct access | 3.469 | 0.196 |
Reflections | 6.505 | 0.620 |
Handle | 7.183 | 1.132 |
Score | Error | |
---|---|---|
Direct access | 3.476 | 0.329 |
Reflections | 5.786 | 0.160 |
Handle | 7.309 | 0.677 |
Turns out the new approach is two times slower than direct access. Surprisingly invoking methods indirectly using
MethodHandle
is slower than the original Reflections API.
Lambda Metafactory
JDK 8 introduced lambda expressions and the world became a better place. A simple listener could now be registered using just one line of code instead of at least six as it was the case with anonymous classes. Complex operations on collections became more readable thanks to the Stream API. Soon reactive libraries began to emerge.
On the surface it seems like lambda expression is just syntactic sugar for anonymous classes. In reality the process is
more complex and it involves both invokedynamic
and code generation during compilation to bytecode.
All the real magic happens in the LambdaMetafactory
class. It is used to transform any direct method handle into an
implementation of the target functional interface.
What's important this class is a part of the public API so there's nothing stopping us from using it in our code. We can
retrieve a Method
using standard Reflections API, unreflect it to MethodHandle
and then use the LambdaMetafactory
to produce an object implementing the desired interface which then can be called as any other code. Implementing the
full transformation process is not that easy but it sure does pay off.
Let's say we have the following code.
String toBeTrimmed = " text with spaces ";
Supplier<String> lambda = toBeTrimmed::trim;
The code below shows how we can replicate the same behaviour ourselves.
String toBeTrimmed = " text with spaces ";
Method reflectionMethod = String.class.getMethod("trim");
Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.unreflect(reflectionMethod);
CallSite callSite = LambdaMetafactory.metafactory(
// method handle lookup
lookup,
// name of the method defined in the target functional interface
"get",
// type to be implemented and captured objects
// in this case the String instance to be trimmed is captured
MethodType.methodType(Supplier.class, String.class),
// type erasure, Supplier will return an Object
MethodType.methodType(Object.class),
// method handle to transform
handle,
// Supplier method real signature (reified)
// trim accepts no parameters and returns String
MethodType.methodType(String.class));
Supplier<String> lambda = (Supplier<String>) callSite.getTarget().bindTo(toBeTrimmed).invoke();
That piece of code is not obvious at all so here's a bit more in depth explanation. LambdaMetafactory
contains the
metafactory postfix for a reason. The metafactory()
method does not return the Supplier instance directly as we'd
expect. Instead it returns a CallSite
object. CallSite.getTarget()
returns a method handle which acts as a factory
for objects implementing the Supplier interface. So in fact LambdaMetafactory is a factory of factories.
The whole process is divided into two steps: creating the factory and using that factory to instantiate the final
object. It directly reflects the process by which lambdas are compiled and executed. Every lambda expression can capture
0 or more values and the CallSite.getTarget
method handle represents the capturing process. We can invoke that method
handle with any number of values we want to have available in the lambda body.
Static method reference Collections::emptyList
doesn't require any external value. Instance method reference
stringInstance::trim
automatically captures the stringInstance
variable. For other cases the number of captured
values is equal to the number of external values used within lambda body.
Let's now see the benchmark results.
Score | Error | |
---|---|---|
Direct access | 3.469 | 0.196 |
Reflections | 6.505 | 0.620 |
Handle | 7.183 | 1.132 |
Lambda | 3.731 | 0.088 |
Score | Error | |
---|---|---|
Direct access | 3.476 | 0.329 |
Reflections | 5.786 | 0.160 |
Handle | 7.309 | 0.677 |
Lambda | 3.563 | 0.044 |
On OpenJDK 11 lambdas created with LambdaMetafactory are almost as fast as direct access itself. On OpenJDK 8 performance is a bit worse but still really good.
Summary
Unsurprisingly normal code beats in terms of performance all other methods of executing code. Whenever you can just go for the simplest solution.
However you can't solve all problems with direct access alone. Frameworks and libraries together with annotations make our life easier. When developing one indirect access is a must and there are several ways of achieving the same goal.
Reflections API is a great starting point. It's well documented and has been around for ages. You'll easily find code examples and libraries filling the base API gaps. While performance is not as good as direct access in many cases it doesn't matter as the overall impact will be low.
If your library often invokes methods indirectly (especially if in tight loops) consider using LambdaMetafactory to
transform Method
instances into functional interfaces. The code is not that simple but it's certainly worth the
trouble.
As for method handles there's no good reason to switch from reflections API unless you're creating your own JVM-based
dynamic language where you don't really have a choice due to the nature of the invokedynamic
bytecode instruction.