一直以来都在说反射慢,但是根本没有具体测试过,也没感受过
反射真的慢吗?
参考:https://www.jianshu.com/p/4e2b49fa8ba1
是的,很慢!
下图是一亿次循环的耗时:
- 直接调用 100000000 times using 36 ms
- 原生反射(只invoke) 100000000 times using 325 ms
- 原生反射(只getMethod) 100000000 times using 11986 ms
- 原生反射(缓存Method) 100000000 times using 319 ms
- 原生反射(没有缓存Method) 100000000 times using 12169 ms
- reflectAsm反射优化(缓存Method) 100000000 times using 43 ms
- reflectAsm反射优化(没有缓存Method) 100000000 times using 131788 ms
没有一个可以比 直接调用 更快的。
- 原生反射(没有缓存Method) 大概比 直接调用 慢了 340倍
- 原生反射(缓存Method) 大概比 直接调用 慢了 9倍
怎么优化速度?
反射的速度差异只在大量连续使用才能明显看出来,理论上100万次才会说反射很慢,对于一个单进单出的请求来说,反射与否根本差不了多少。
这样就没必要优化了吗,并不是。
事实上各大框架注解,甚至业务系统中都在使用反射,不能因为慢就不用了。
在后台Controller中序列化请求响应信息大量使用注解,高并发就意味着连续百万级别调用反射成为可能,各大MVC框架都会着手解决这个问题,优化反射。
反射核心的是getMethod和invoke了,分析下两者的耗时差距,在一亿次循环下的耗时。
Method getName = SimpleBean.class.getMethod("getName");
getName.invoke(bean);
原生反射(只invoke) 100000000 times using 221 ms
原生反射(只getMethod) 100000000 times using 12849 ms
优化思路1:缓存Method,不重复调用getMethod
证明getMethod很耗时,所以说我们要优先优化getMethod,看看为什么卡?
Method getName = SimpleBean.class.getMethod("getName");
//查看源码
Method res = privateGetMethodRecursive(name, parameterTypes, includeStaticMethods, interfaceCandidates);
//再看下去
private native Field[] getDeclaredFields0(boolean publicOnly);
private native Method[] getDeclaredMethods0(boolean publicOnly);
private native Constructor<T>[] getDeclaredConstructors0(boolean publicOnly);
private native Class<?>[] getDeclaredClasses0();
getMethod最后直接调用native方法,无解了。想复写优化getMethod是不可能的了,官方没毛病。
但是我们可以不需要每次都getMethod啊,我们可以缓存到redis,或者放到Spring容器中,就不需要每次都拿了。
//通过Java Class类自带的反射获得Method测试,仅进行一次method获取
@Test
public void javaReflectGetOnly() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Method getName = SimpleBean.class.getMethod("getName");
Stopwatch watch = Stopwatch.createStarted();
for (long i = 0; i < times; i++) {
getName.invoke(bean);
}
watch.stop();
String result = String.format(formatter, "原生反射+缓存Method", times, watch.elapsed(TimeUnit.MILLISECONDS));
System.out.println(result);
}
- 原生反射(缓存Method) 100000000 times using 319 ms
- 原生反射(没有缓存Method) 100000000 times using 12169 ms
缓存Method大概快了38倍,离原生调用还差个9倍,所以我们继续优化invoke。
优化思路2:使用reflectAsm,让invoke变成直接调用
我们看下invoke的源码:
getName.invoke(bean);
//查看源码
private static native Object invoke0(Method var0, Object var1, Object[] var2);
尴尬,最后还是native方法,依然没毛病。
invoke不像getMethod可以缓存起来重复用,没法优化。
所以这里需要引入ASM,并做了个工具库reflectAsm:
参考:https://blog.csdn.net/zhuoxiuwu/article/details/78619645,https://github.com/EsotericSoftware/reflectasm
“ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。”
使用如下:
MethodAccess methodAccess = MethodAccess.get(SimpleBean.class);
methodAccess.invoke(bean, "getName");
//看看MethodAccess.get(SimpleBean.class)源码,使用了反射的getMethod】
Method[] declaredMethods = type.getDeclaredMethods();
参考:https://blog.csdn.net/z69183787/article/details/51657771
invoke是没办法优化的,也没办法做到像直接调用那么快。所以大佬们脑洞大开,不用反射的invoke了。原理如下:
- 借反射的getDeclaredMethods获取SimpleBean.class的所有方法,然后动态生成一个继承于MethodAccess 的子类SimpleBeanMethodAccess,动态生成一个Class文件并load到JVM中。
- SimpleBeanMethodAccess中所有方法名建立index索引,index跟方法名是映射的,根据方法名获得index,SimpleBeanMethodAccess内部建立的switch直接分发执行相应的代码,这样methodAccess.invoke的时候,实际上是直接调用。
实际上reflectAsm是有个致命漏洞的,因为要生成文件,还得load进JVM,所以reflectAsm的getMethod特别慢:
- reflectAsm反射优化(没有缓存Method) 100000000 times using 131788 ms
虽然getMethod很慢,但是invoke的速度是到达了直接调用的速度了。
如果能够缓存method,那么reflectAsm的速度跟直接调用一样,而且能够使用反射!
- 直接调用 100000000 times using 36 ms
- reflectAsm反射优化(缓存Method) 100000000 times using 43 ms
- 这其中差的7ms,是reflectAsm生成一次Class文件的损耗。
下面是反射优化的测试样例:
//通过高性能的ReflectAsm库进行测试,仅进行一次methodAccess获取
@Test
public void reflectAsmGetOnly() {
MethodAccess methodAccess = MethodAccess.get(SimpleBean.class);
Stopwatch watch = Stopwatch.createStarted();
for (long i = 0; i < times; i++) {
methodAccess.invoke(bean, "getName");
}
watch.stop();
String result = String.format(formatter, "reflectAsm反射优化+缓存Method", times, watch.elapsed(TimeUnit.MILLISECONDS));
System.out.println(result);
}
网友评论