背景
在使用Java进行开发时,我们会不可避免的使用到大量的反射操作,比如Spring Boot
会在接收到HTTP
请求时,利用反射Controller
调用接口中的对应方法,或是Jackson
框架使用反射来解析json
中的数据给对应字段进行赋值,我们可以编写一个简单的JMH测试来评估一下通过反射调用来创建对象的性能,与直接调用对象构造方法之间的差距:
@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); }
}
测试代码使用了Java Microbenchmark Harness (JMH)的注解,用于标记和配置一个微基准测试类。JMH是一个用于编写、运行和分析Java微基准测试的框架。它可以帮助你编写出更可靠、可重复的基准测试。有兴趣的小伙伴可以去了解一下
在Test
类中,具有一个简单的int类型的变量,我们分别测试直接调用构造方法,赋值然后取值,以及使用Constructor
和Method
进行普通反射调用之间的性能对比,注意一定要将构造出来的对象使用Blackhole.consume()
方法给吃掉,这样JVM才不会把没有使用到的变量给直接的优化掉,得出错误的测试结果,以上代码在笔者的机器上运行的结果如下:
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
可能有些小伙伴对JMH并不熟悉,看不懂运行结果,我大概阐述下运行结果表头含义。
Benchmark
这是基准测试方法的名称。上面的例子就是testDirectCall
和testNormalReflection
。(size)
是参数化测试中使用的参数名称。在上面的例子中,size参数有四个不同的值(10, 100, 1000, 10000),用于控制基准测试中循环的次数或对象的大小等。JMH会为每一个参数值运行基准测试,并给出相应的结果。Mode
:这表示测量的模式。在我们上面的例子中,它被设置为AverageTime
,表示测量的是每次迭代的平均执行时间。JMH支持多种测量模式,如吞吐量(每秒操作数)、平均时间、单次操作时间等。Cnt
:这表示迭代次数。它指的是JMH在预热阶段和测量阶段中运行的测试方法迭代次数。注意,这里的迭代次数可能与我们在注解中设置的迭代次数不同,因为JMH可能会为了获得更稳定的结果而自动调整迭代次数。Score
:这是主要的性能指标。在你的例子中,它表示每次迭代的平均执行时间。这是基准测试方法性能的主要度量标准。Error
:这表示Score的误差范围。它表示Score值的置信区间,通常用于评估测量结果的稳定性和可靠性。误差范围越小,说明测量结果越稳定。Units
:这表示Score和Error的单位。在上面的例子中,单位是Units,这实际上是一个占位符,因为我们在@OutputTimeUnit(TimeUnit.NANOSECONDS)注解中指定了输出单位为纳秒(NANOSECONDS)。所以,正确的单位应该是ns(纳秒)。
可以看到,使用反射的性能比起直接调用来讲有非常大的差距,尤其是在这种极其简单的对象创建场景中,但是使用反射是很多情况下我们不得不采用的一个做法,那么我们有没有什么办法来尽可能优化一下反射调用的性能呢?
先让我们试一下MethodHandle
提供的方法调用模型,MethodHandle
是自JDK7版本后开始推出的,用于替换旧反射调用的新方式,相比起原有的反射调用,提供了更多的交互方式,并且具备对Java方法调用和Native方法调用一致的模型,我们可以简单的创建一个用例进行测试:
@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(); }
}
实测的结果则更加的不尽人意:
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
可以看到,使用MethodHandle
与使用普通反射之间的性能差距,就和普通反射与直接调用之间的差距一样大,事实上在JDK18以后,根据# JEP 416: Reimplement Core Reflection with Method Handles 使用java.lang.reflect
和java.lang.invoke
的相关API已经进行了相应的底层重构,转而使用MethodHandle
进行实现,很明显,在使用java.lang.reflect
和java.lang.invoke
中的方法时,与直接使用MethodHandle
相比,具备了更多的优化工作,根据官方的说法,在使用MethodHandle
时因将字段尽可能定义为static final
,这样JVM可以将其进行常量折叠,从而实现巨大的性能提升,让我们修改一下以上的测试代码:
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(); }
}
得到了如下的数据:
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
突然之间,我们的反射调用和直接调用的性能已经完全一致了,那么这是不是意味着,我们想要的功能已经完全实现了呢?事实上并未如此,如果我们必须在static final
中指定需要使用到的反射字段,那么就相当于损失了绝大多数的灵活性,在实际操作中可行性并不高。
同样的,我们可以试一试,将直接使用java.lang.reflect
和java.lang.invoke
的函数所需的对象先构建并缓存在本地,再测试一下其对应的性能:
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
时我们将需要初始化的变量定义为static final
不同,此处我们直接将其定义为private
变量,在JMH框架中提供的@Setup
函数中进行初始化,更贴合的模拟我们在运行时进行创建的行为,测试得到的结果如下:
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
可以看到,使用普通反射的方式,无论是每次都获取新的Constructor
或Method
对象进行创建,还是通过提前缓存的形式进行加载,性能表现是相似的,这也使得通用的反射调用方式在各类通用场景下都能够具备比较不错的表现。
鉴于我们之前的这些测试结果,如果想要进一步的提升反射的性能,只能考虑使用类生成的方式,在编译期创建出MethodHandle
的静态变量,让JVM帮我们去自动内联,当然,类生成的方式一定可以拥有非常不错的性能,但是使用ByteBuddy
或Asm
框架进行类生成的代码相对而言过于繁琐,目前# JEP 457: Class-File API (Preview) 特性正处于preview
阶段,可以帮助我们更加简化的在JVM中进行类生成,但是目前我们还无法对其进行使用。
解决方案
Lambda
表达式贯穿了我们日常的开发中的所有角落,且Lambda
表达式本身的性能不会差,否则JDK内部绝对不会如此大量的使用它,Lambda
表达式的生成方式也并不复杂,其背后的核心方法是通过LambdaMetafactory.metafactory()
方法生成对应的方法调用,我们可是实现以下的代码来完成对应构造函数,getter
方法和setter
方法向Lambda
函数的转换:
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));
}
测试分为两个步骤,一个是测试Lambda
表达式的生成性能,一个是测试Lambda
表达式的运行性能,这两个指标对我们来说都非常的重要,得到的结果如下:
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
可以看到,通过模拟Lambda
表达式生成的方式,调用构造函数以及get
和set
方法的性能,与直接调用是几乎完全一致的,这也就达成了我们想要的效果,但是Lambda
生成的性能非常不容乐观,与直接使用箭头函数进行生成的性能有着天壤之别,好在如果Lambda
表达式没有捕获任何的外部变量,比如我们在示例中调用的get
和set
方法,那么生成的方法是可以被缓存起来重复使用的,如果使用的基数本身比较大,在多次调用的开销权衡中,初始化的开销就可以被忽略不计。
小结
本文介绍了一种在Java
中的新的反射调用方式,即使用类似于Lambda
表达式的生成的方式进行反射,可以将一些简单的方法,例如get
和set
方法,直接转化为相应的Lambda
表达式来调用,虽然可以做到和直接调用一致的性能,但是该方法的生成开销比较大,需要在频繁调用的场景中进行缓存,才能起到比较好的效果。