反射的性能差在哪里?

一直以来都在说反射慢,但是根本没有具体测试过,也没感受过

反射真的慢吗?

参考: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[] 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);
    }

你可能感兴趣的:(反射的性能差在哪里?)