反射真的慢吗?
是的,很慢!

下图是一亿次循环的耗时:

直接调用 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:

“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();

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);
    }