background
When Java
developing, we will inevitably use a large number of reflection operations. For example, when Spring Boot
receiving HTTP
a request, we will use reflection Controller
to call the corresponding method in the interface, or Jackson
the framework will use reflection to parse json
the data and assign values to the corresponding fields. , we can write a simple JMH
test to evaluate the performance gap between creating objects through reflective calls and directly calling object constructors:
@BenchmarkMode(value = Mode.AverageTime)
@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public abstract class JmhTest {
public static void runTest(Class<?> launchClass) throws RunnerException {
Options options = new OptionsBuilder().include(launchClass.getSimpleName()).build();
new Runner(options).run();
}
}
package cn.zorcc.common.jmh;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.RunnerException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class ReflectionTest extends JmhTest {
@Param({"10", "100", "1000", "10000"})
private int size;
static class Test {
private int integer;
public int getInteger() {
return integer;
}
public void setInteger(int integer) {
this.integer = integer;
}
}
@Benchmark
public void testDirectCall(Blackhole bh) {
for(int i = 0; i < size; i++) {
Test test = new Test();
bh.consume(test);
test.setInteger(i);
bh.consume(test.getInteger());
}
}
@Benchmark
public void testNormalReflection(Blackhole bh) {
try{
Constructor<Test> constructor = Test.class.getDeclaredConstructor();
Method setter = Test.class.getDeclaredMethod("setInteger", int.class);
Method getter = Test.class.getDeclaredMethod("getInteger");
for(int i = 0; i < size; i++) {
Test test = constructor.newInstance();
bh.consume(test);
setter.invoke(test, i);
int integer = (int) getter.invoke(test);
bh.consume(integer);
}
}catch (Throwable e) {
throw new UnknownError();
}
}
public static void main(String[] args) throws RunnerException {
runTest(ReflectionTest.class);
}
}
In Test
the class, there is a simple int
type of variable. We test the performance comparison between directly calling the constructor, assigning and then getting the value, and using Constructor
and making ordinary reflection calls. Be sure to use Method
the constructed object. Blackhole.consume()
This way, JVM
unused variables will not be directly optimized and wrong test results will be obtained. The results of running the above code on the author’s machine are as follows:
Benchmark (size) Mode Cnt Score Error Units
ReflectionTest.testDirectCall 10 avgt 50 10.584 ± 0.141 ns/op
ReflectionTest.testDirectCall 100 avgt 50 108.301 ± 1.129 ns/op
ReflectionTest.testDirectCall 1000 avgt 50 1068.026 ± 12.312 ns/op
ReflectionTest.testDirectCall 10000 avgt 50 10660.596 ± 148.673 ns/op
ReflectionTest.testNormalReflection 10 avgt 50 145.483 ± 1.300 ns/op
ReflectionTest.testNormalReflection 100 avgt 50 1131.994 ± 19.586 ns/op
ReflectionTest.testNormalReflection 1000 avgt 50 13461.067 ± 130.624 ns/op
ReflectionTest.testNormalReflection 10000 avgt 50 148811.318 ± 5766.679 ns/op
It can be seen that there is a very big gap in the performance of using reflection compared to direct calling, especially in this extremely simple object creation scenario, but using reflection is a method we have to adopt in many cases, so we Is there any way to optimize the performance of reflection calls as much as possible?
Let us first try MethodHandle
the provided method calling model, MethodHandle
which has JDK7
been launched since the version. It is a new way to replace the old reflection calling. Compared with the original reflection calling, it provides more interaction methods and has the ability to support Java
methods. Calling Native
a consistent model with method calls, we can simply create a use case for testing:
@Benchmark
public void testMethodHandleReflection(Blackhole bh) {
try{
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType constructorType = MethodType.methodType(void.class);
MethodHandle constructorHandle = lookup.findConstructor(Test.class, constructorType);
MethodHandle iSetter = lookup.findSetter(Test.class, "integer", int.class);
MethodHandle iGetter = lookup.findGetter(Test.class, "integer", int.class);
for(int i = 0; i < size; i++) {
Test test = (Test) constructorHandle.invokeExact();
bh.consume(test);
iSetter.invokeExact(test, i);
int integer = (int) iGetter.invokeExact(test);
bh.consume(integer);
}
}catch (Throwable e) {
throw new UnknownError();
}
}
The actual measurement results are even more unsatisfactory:
ReflectionTest.testMethodHandleReflection 10 avgt 50 1346.515 ± 17.347 ns/op
ReflectionTest.testMethodHandleReflection 100 avgt 50 2355.083 ± 37.358 ns/op
ReflectionTest.testMethodHandleReflection 1000 avgt 50 456694.572 ± 31415.118 ns/op
ReflectionTest.testMethodHandleReflection 10000 avgt 50 982008.110 ± 46807.572 ns/op
It can be seen that MethodHandle
the performance gap between using and using ordinary reflection is as big as the gap between ordinary reflection and direct call. In fact, in JDK18
the future, according to #JEP 416: Reimplement Core Reflection with Method Handles, the correlation between using java.lang.reflect
and Corresponding underlying refactoring was carried out and implemented using Define it as much as possible , so that it can be constant-folded, thereby achieving a huge performance improvement. Let us modify the above test code:java.lang.invokeAPIMethodHandlejava.lang.reflectjava.lang.invokeMethodHandleMethodHandlestatic finalJVM
private static final MethodHandle constructorHandle;
private static final MethodHandle iSetter;
private static final MethodHandle iGetter;
static {
try{
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType constructorType = MethodType.methodType(void.class);
constructorHandle = lookup.findConstructor(Test.class, constructorType);
iSetter = lookup.findSetter(Test.class, "integer", int.class);
iGetter = lookup.findGetter(Test.class, "integer", int.class);
}catch (Throwable e) {
throw new UnknownError();
}
}
@Benchmark
public void testMethodHandleReflection(Blackhole bh) {
try{
for(int i = 0; i < size; i++) {
Test test = (Test) constructorHandle.invokeExact();
bh.consume(test);
iSetter.invokeExact(test, i);
int integer = (int) iGetter.invokeExact(test);
bh.consume(integer);
}
}catch (Throwable e) {
throw new UnknownError();
}
}
The following data was obtained:
ReflectionTest.testMethodHandleReflection 10 avgt 50 9.825 ± 0.084 ns/op
ReflectionTest.testMethodHandleReflection 100 avgt 50 99.174 ± 1.128 ns/op
ReflectionTest.testMethodHandleReflection 1000 avgt 50 997.094 ± 11.961 ns/op
ReflectionTest.testMethodHandleReflection 10000 avgt 50 10212.014 ± 215.662 ns/op
Suddenly, the performance of our reflection call and direct call are completely consistent, so does this mean that the function we want has been fully realized? In fact, this is not the case. If we have to static final
specify the reflection fields that need to be used, it is equivalent to losing most of the flexibility, and the feasibility in actual operations is not high.
Similarly, we can give it a try, first build and cache the objects required for direct use of java.lang.reflect
functions java.lang.invoke
locally, and then test their corresponding performance:
private Constructor<Test> c;
private Method setter;
private Method getter;
@Setup
public void setup() {
try{
this.c = Test.class.getDeclaredConstructor();
this.setter = Test.class.getDeclaredMethod("setInteger", int.class);
this.getter = Test.class.getDeclaredMethod("getInteger");
}catch (Throwable e) {
throw new UnknownError();
}
}
@Benchmark
public void testNormalReflection(Blackhole bh) {
try{
for(int i = 0; i < size; i++) {
Test test = c.newInstance();
bh.consume(test);
setter.invoke(test, i);
int integer = (int) getter.invoke(test);
bh.consume(integer);
}
}catch (Throwable e) {
throw new UnknownError();
}
}
MethodHandle
Unlike when we define the variables that need to be initialized when testing static final
, here we directly define them as private
variables and initialize them in the functions JMH
provided in the framework @Setup
, which more closely simulates the behavior we create at runtime. The test gets The result is as follows:
ReflectionTest.testNormalReflection 10 avgt 50 152.242 ± 5.625 ns/op
ReflectionTest.testNormalReflection 100 avgt 50 1495.302 ± 21.467 ns/op
ReflectionTest.testNormalReflection 1000 avgt 50 16917.774 ± 420.810 ns/op
ReflectionTest.testNormalReflection 10000 avgt 50 143252.377 ± 2150.908 ns/op
It can be seen that using ordinary reflection, whether it is to obtain a new object for creation each time Constructor
or Method
to load it through advance caching, the performance is similar, which also makes the general reflection calling method applicable to all types of applications. It can perform relatively well in any scenario.
In view of our previous test results, if we want to further improve the performance of reflection, we can only consider using the class generation method. MethodHandle
The static variables created during compilation can be JVM
automatically inlined for us. Of course, the class generation method It must have very good performance, but the code for class generation using ByteBuddy
or Asm
framework is relatively cumbersome. Currently [# JEP 457: Class-File API (Preview)].( openjdk.org/jeps/457 ) The feature is in preview
stage, which can help us more simplify JVM
class generation in , but we can’t use it yet.
solution
Lambda
Expressions run through all corners of our daily development, and Lambda
the performance of the expression itself is not bad, otherwise JDK
it would never be used so extensively internally. Lambda
The expression generation method is not complicated either. The core method behind it is Through LambdaMetafactory.metafactory()
methods to generate corresponding method calls, we can implement the following code to complete the conversion of corresponding constructors, getter
methods and setter
methods to Lambda
functions:
private Supplier<Test> constructor;
private BiConsumer<Object, Object> setConsumer;
private Function<Test, Integer> getFunction;
@Setup
public void setup() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(ReflectionTest.class, MethodHandles.lookup());
this.constructor = lambdaGenerateConstructor(lookup);
this.setConsumer = lambdaGenerateSetter(lookup);
this.getFunction = lambdaGenerateGetter(lookup);
}
@SuppressWarnings("unchecked")
private Supplier<Test> lambdaGenerateConstructor(MethodHandles.Lookup lookup) throws Throwable {
MethodHandle cmh = lookup.findConstructor(Test.class, MethodType.methodType(void.class));
CallSite c1 = LambdaMetafactory.metafactory(lookup,
"get",
MethodType.methodType(Supplier.class),
MethodType.methodType(Object.class), cmh, MethodType.methodType(Test.class));
return (Supplier<Test>) c1.getTarget().invokeExact();
}
@SuppressWarnings("unchecked")
private BiConsumer<Object, Object> lambdaGenerateSetter(MethodHandles.Lookup lookup) throws Throwable {
MethodHandle setHandle = lookup.findVirtual(Test.class, "setInteger", MethodType.methodType(void.class, int.class));
CallSite callSite = LambdaMetafactory.metafactory(lookup,
"accept",
MethodType.methodType(BiConsumer.class),
MethodType.methodType(void.class, Object.class, Object.class),
setHandle,
MethodType.methodType(void.class, Test.class, Integer.class));
return (BiConsumer<Object, Object>) callSite.getTarget().invokeExact();
}
@SuppressWarnings("unchecked")
private Function<Test, Integer> lambdaGenerateGetter(MethodHandles.Lookup lookup) throws Throwable {
MethodHandle getHandle = lookup.findVirtual(Test.class, "getInteger", MethodType.methodType(int.class));
CallSite getSite = LambdaMetafactory.metafactory(
lookup,
"apply",
MethodType.methodType(Function.class),
MethodType.methodType(Object.class, Object.class),
getHandle,
MethodType.methodType(Integer.class, Test.class)
);
return (Function<Test, Integer>) getSite.getTarget().invokeExact();
}
@Benchmark
public void testLambda(Blackhole bh) {
for(int i = 0; i < size; i++) {
Test test = constructor.get();
bh.consume(test);
setConsumer.accept(test, i);
int integer = getFunction.apply(test);
bh.consume(integer);
}
}
@Benchmark
public void testLambdaGeneration(Blackhole bh) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(ReflectionTest.class, MethodHandles.lookup());
bh.consume(lambdaGenerateConstructor(lookup));
bh.consume(lambdaGenerateSetter(lookup));
bh.consume(lambdaGenerateGetter(lookup));
}
The test is divided into two steps. One is to test Lambda
the generation performance of the expression, and the other is to test Lambda
the running performance of the expression. These two indicators are very important to us. The results are as follows:
ReflectionTest.testLambdaGeneration 10000 avgt 50 92486.909 ± 62638.147 ns/op
Benchmark (size) Mode Cnt Score Error Units
ReflectionTest.testLambda 10 avgt 50 10.720 ± 0.087 ns/op
ReflectionTest.testLambda 100 avgt 50 105.001 ± 1.312 ns/op
ReflectionTest.testLambda 1000 avgt 50 1020.406 ± 9.990 ns/op
ReflectionTest.testLambda 10000 avgt 50 10198.842 ± 143.259 ns/op
It can be seen that by simulating Lambda
expression generation, the performance of calling constructors and get
methods set
is almost exactly the same as direct calling. This achieves the effect we want, but the Lambda
generated performance is very unoptimistic. The performance of directly using arrow functions to generate is very different. Fortunately, if Lambda
the expression does not capture any external variables, such as the get
and set
method we called in the example, then the generated method can be cached and reused. If you use The base itself is relatively large, and in the cost trade-off of multiple calls, the initialization overhead can be ignored.
summary
This article introduces a Java
new reflection calling method in , that is, using Lambda
a method similar to expression generation for reflection. Some simple methods, such as get
and set
methods, can be directly converted into corresponding Lambda
expressions for calling. Although it can It can achieve the same performance as direct calling, but the generation overhead of this method is relatively large, and it needs to be cached in frequent calling scenarios to achieve better results.