java反射性能-调研

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

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

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

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

            cglib、orika、spring等bean copy工具性能测试和原理分析

                在实际项目中,考虑到不同的数据使用者,我们经常要处理 VO、DTO、Entity、DO 等对象的转换,如果手动编写 setter/getter 方法一个个赋值,将非常繁琐且难维护。通常情况下,这类转换都是同名属性的转换(类型可以不同),我们更多地会使用 bean copy 工具,例如 Apache Commons BeanUtils、Cglib BeanCopier 等。
            

            在使用 bean copy 工具时,我们更多地会考虑性能,有时也需要考虑深浅复制的问题。本文将对比几款常用的 bean copy 工具的性能,并介绍它们的原理、区别和使用注意事项。

            简介

            在实际项目中,考虑到不同的数据使用者,我们经常要处理 VO、DTO、Entity、DO 等对象的转换,如果手动编写 setter/getter 方法一个个赋值,将非常繁琐且难维护。通常情况下,这类转换都是同名属性的转换(类型可以不同),我们更多地会使用 bean copy 工具,例如 Apache Commons BeanUtils、Cglib BeanCopier 等。

            在使用 bean copy 工具时,我们更多地会考虑性能,有时也需要考虑深浅复制的问题。本文将对比几款常用的 bean copy 工具的性能,并介绍它们的原理、区别和使用注意事项

            项目环境

            本文使用 jmh 作为测试工具。

            os:win 10

            jdk:1.8.0_231

            jmh:1.25

            选择的 bean copy 工具及对应的版本如下:

            apache commons beanUtils:1.9.4

            spring beanUtils:5.2.10.RELEASE

            cglib beanCopier:3.3.0

            orika mapper:1.5.4

            测试代码

            本文使用的 java bean 如下,这个是之前测试序列化工具时用过的。一个用户对象,一对一关联部门对象和岗位对象,其中部门对象又存在自关联。

            public class User implements Serializable {
                private static final long serialVersionUID = 1L;
                // 普通属性--129个
                private String id;
                private String account;
                private String password;
                private Integer status;
                // ······
            
            <span class="hljs-comment">/**
             * 所属部门
             */</span>
            <span class="hljs-keyword">private</span> Department department;
            <span class="hljs-comment">/**
             * 岗位
             */</span>
            <span class="hljs-keyword">private</span> Position position;
            
            <span class="hljs-comment">// 以下省略setter/getter方法</span>
            

            }
            public class Department implements Serializable {
            private static final long serialVersionUID = 1L;
            // 普通属性–7个
            private String id;
            private String parentId;
            // ······
            /**
            * 子部门
            */

            private List<Department> children;

            <span class="hljs-comment">// 以下省略setter/getter方法</span>
            

            }
            public class Position implements Serializable {
            private static final long serialVersionUID = 1L;
            // 普通属性–6个
            private String id;
            private String name;
            // ······
            // 以下省略setter/getter方法
            }

            下面展示部分测试代码,完整代码见末尾链接。

            apache commons beanUtils

            apache commons beanUtils 的 API 非常简单,通常只要一句代码就可以了。它支持自定义转换器(这个转换器是全局的,将替代默认的转换器)。

                @Benchmark
                public UserVO testApacheBeanUtils(CommonState commonState) throws Exception {
                    /*ConvertUtils.register(new Converter() {
                        @Override
                        public <T> T convert(Class<T> type, Object value) {
                            if (Boolean.class.equals(type) || boolean.class.equals(type)) {
                                final String stringValue = value.toString().toLowerCase();
                                for (String trueString : trueStrings) {
                                    if (trueString.equals(stringValue)) {
                                        return type.cast(Boolean.TRUE);
                                    }
                                }
                                // ······
                            }
                            return null;
                        }
                    }, Boolean.class);*/
                    UserVO userVO = new UserVO();
                    org.apache.commons.beanutils.BeanUtils.copyProperties(userVO, commonState.user);
                    assert "zzs0".equals(userVO.getName());
                    return userVO;
                }
            

            apache commons beanUtils 的原理比较简单,浓缩起来就是下面的几行代码。可以看到,源对象属性值的获取、目标对象属性值的设置,都是使用反射实现,所以,apache commons beanUtils 的性能稍差。还有一点需要注意,它的复制只是浅度复制

                    // 获取目标类的BeanInfo对象(这个会缓存起来,不用每次都重新创建)
                    BeanInfo targetBeanInfo = Introspector.getBeanInfo(target.getClass());
                    // 获取目标类的PropertyDescriptor数组(这个会缓存起来,不用每次都重新创建)
                    PropertyDescriptor[] targetPds = targetBeanInfo.getPropertyDescriptors();
            
                <span class="hljs-comment">// 遍历PropertyDescriptor数组,并给同名属性赋值</span>
                <span class="hljs-keyword">for</span>(PropertyDescriptor targetPd : targetPds) {
                    <span class="hljs-comment">// 获取源对象中同名属性的PropertyDescriptor对象,当然,这个也是通过Introspector获取的</span>
                    <span class="hljs-type">PropertyDescriptor</span> <span class="hljs-variable">sourcePd</span> <span class="hljs-operator">=</span> getPropertyDescriptor(source.getClass(), targetPd.getName());
                    <span class="hljs-comment">// 读取源对象中该属性的值</span>
                    <span class="hljs-type">Method</span> <span class="hljs-variable">readMethod</span> <span class="hljs-operator">=</span> sourcePd.getReadMethod();
                    <span class="hljs-type">Object</span> <span class="hljs-variable">value</span> <span class="hljs-operator">=</span> readMethod.invoke(source);
                    <span class="hljs-comment">// 设置目标对象中该属性的值</span>
                    <span class="hljs-type">Method</span> <span class="hljs-variable">writeMethod</span> <span class="hljs-operator">=</span> targetPd.getWriteMethod();
                    writeMethod.invoke(target, value);
                }
            

            spring beanUtils

            spring beanUtils 的 API 和 apache commons beanUtils 差不多,也是简单的一句代码。但是,前者只支持同类型属性的转换,且不支持自定义转换器

                @Benchmark
                public UserVO testSpringBeanUtils(CommonState commonState) throws Exception {
                    UserVO userVO = new UserVO();
                    org.springframework.beans.BeanUtils.copyProperties(commonState.user, userVO);
                    assert "zzs0".equals(userVO.getName());
                    return userVO;
                }
            

            看过 spring beanUtils 源码就会发现,它只是一个简单的工具类,只有短短几行代码。原理的话,和 apache commons beanUtils 一样的,所以,它的复制也是浅度复制

            cglib beanCopier

            cglib beanCopier 需要先创建一个BeanCopier,然后再执行 copy 操作。它也支持设置自定义转换器,但是,这种转换器仅限当前调用有效,而且,我们需要在同一个转换器里处理所有类型的转换。使用 cglib beanCopier 需要注意,BeanCopier对象可复用,不需要重复创建

                @Benchmark
                public UserVO testCglibBeanCopier(CommonState commonState, CglibBeanCopierState cglibBeanCopierState) throws Exception {
                    BeanCopier copier = cglibBeanCopierState.copier;
                    UserVO userVO = new UserVO();
                    copier.copy(commonState.user, userVO, null);
                    assert "zzs0".equals(userVO.getName());
                    return userVO;
                }
            
            <span class="hljs-meta">@State(Scope.Benchmark)</span>
            <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">CglibBeanCopierState</span> {
                BeanCopier copier;
                <span class="hljs-meta">@Setup(Level.Trial)</span>
                <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">prepare</span><span class="hljs-params">()</span> {
                    copier = BeanCopier.create(User.class, UserVO.class, <span class="hljs-literal">false</span>);
                }
            }
            

            cglib beanCopier 的原理也不复杂,它是使用了 asm 生成一个包含所有 setter/getter 代码的代理类,通过设置以下系统属性可以在指定路径输出生成的代理类:

            cglib.debugLocation=D:/growUp/test
            

            打开上面例子生成的代理类,可以看到,源对象属性值的获取、目标对象属性值的设置,都是直接调用对应方法,而不是使用反射,通过后面的测试会发现它的速度接近我们手动 setter/getter。另外,cglib beanCopier 也是浅度复制

            
            public class Object$$BeanCopierByCGLIB$$6bc9202f extends BeanCopier
            {
                public void copy(final Object o, final Object o2, final Converter converter) {
                    final UserVO userVO = (UserVO)o2;
                    final User user = (User)o;
                    userVO.setAccount(user.getAccount());
                    userVO.setAddress(user.getAddress());
                    userVO.setAge(user.getAge());
                    userVO.setBirthday(user.getBirthday());
                    userVO.setDepartment(user.getDepartment());
                    userVO.setDiploma(user.getDiploma());
                    // ······
                }
            }
            

            orika mapper

            相比其他 bean copy 工具,orika mapper 的 API 要复杂一些,相对地,它的功能也更强大,不仅支持注册自定义转换器,还支持注册对象工厂、过滤器等。使用 orika mapper 需要注意,MapperFactory对象可复用,不需要重复创建

                @Benchmark
                public UserVO testOrikaBeanCopy(CommonState commonState, OrikaState orikaState) throws Exception {
                    MapperFacade mapperFacade = orikaState.mapperFactory.getMapperFacade();// MapperFacade对象始终是同一个
                    UserVO userVO = mapperFacade.map(commonState.user, UserVO.class);
                    assert "zzs0".equals(userVO.getName());
                    return userVO;
                }
                @State(Scope.Benchmark)
                public static class OrikaState {
                    MapperFactory mapperFactory;
                    @Setup(Level.Trial)
                    public void prepare() {
                        mapperFactory = new DefaultMapperFactory.Builder().build();
                        /*mapperFactory.getConverterFactory().registerConverter(new CustomConverter<Boolean, Integer>() {
                            @Override
                            public Integer convert(Boolean source, Type<? extends Integer> destinationType, MappingContext mappingContext) {
                                if(source == null) {
                                    return null;
                                }
                                return source ? 1 : 0;
                            }
                        });*/
                    }
                }
            

            orika mapper 和 cglib beanCopier 有点类似,也会生成包含所有 setter/getter 代码的代理类,不同的是 orika mapper 使用的是 javassist,而 cglib beanCopier 使用的是 asm

            通过设置以下系统属性可以在指定路径输出生成的代理类(本文选择直接输出java文件):

            # 输出java文件
            ma.glasnost.orika.GeneratedSourceCode.writeSourceFiles=true
            ma.glasnost.orika.writeSourceFilesToPath=D:/growUp/test
            # 输出class文件
            # ma.glasnost.orika.GeneratedSourceCode.writeClassFiles=true
            # ma.glasnost.orika.writeClassFilesToPath=D:/growUp/test
            

            和 cglib beanCopier 不同,orika mapper 生成了三个文件。根本原因在于 orika mapper 是深度复制,用户对象中的部门对象和岗位对象也会生成新的实例对象并拷贝属性。

            ![在这里插入图片描述](https://img-blog.csdnimg.cn/202d664ba8dc48c2ab6b53fd1951e28b.png)

            打开其中一个文件,可以看到,普通属性直接赋值,像部门对象这种,会调用BoundMapperFacade继续拷贝。

            public class Orika_UserVO_User_Mapper166522553009000$0 extends ma.glasnost.orika.impl.GeneratedMapperBase {
            public void mapAtoB(java.lang.Object a, java.lang.Object b, ma.glasnost.orika.MappingContext mappingContext) {
            
                super.mapAtoB(a, b, mappingContext);
                // sourceType: User
                cn.zzs.bean.copy.other.User source = ((cn.zzs.bean.copy.other.User)a);
                // destinationType: UserVO
                cn.zzs.bean.copy.other.UserVO destination = ((cn.zzs.bean.copy.other.UserVO)b);
            
                destination.setAccount(((java.lang.String)source.getAccount()));
                destination.setAddress(((java.lang.String)source.getAddress()));
                destination.setAge(((java.lang.Integer)source.getAge()));
                if(!(((cn.zzs.bean.copy.other.Department)source.getDepartment()) == null)) {
                    if(((cn.zzs.bean.copy.other.Department)destination.getDepartment()) == null) {
                        destination.setDepartment((cn.zzs.bean.copy.other.Department)((ma.glasnost.orika.BoundMapperFacade)usedMapperFacades[0]).map(((cn.zzs.bean.copy.other.Department)source.getDepartment()), mappingContext));
                    } else {
                        destination.setDepartment((cn.zzs.bean.copy.other.Department)((ma.glasnost.orika.BoundMapperFacade)usedMapperFacades[0]).map(((cn.zzs.bean.copy.other.Department)source.getDepartment()), ((cn.zzs.bean.copy.other.Department)destination.getDepartment()), mappingContext));
                    }
                } else {
                    {
                        destination.setDepartment(null);
                    }
                }
            
                // ······
            
                if(customMapper != null) {
                    customMapper.mapAtoB(source, destination, mappingContext);
                }
            }
            
            public void mapBtoA(java.lang.Object a, java.lang.Object b, ma.glasnost.orika.MappingContext mappingContext) {
                // ······
            }
            

            }

            测试结果

            以下以吞吐量作为指标,相同条件下,吞吐量越大越好。

            cmd 指令如下:

            mvn clean package
            java -ea -jar target/benchmarks.jar -f 1 -t 1 -wi 10 -i 10
            

            测试结果如下:

            # JMH version: 1.25
            # VM version: JDK 1.8.0_231, Java HotSpot(TM) 64-Bit Server VM, 25.231-b11
            # VM invoker: D:\growUp\installation\jdk1.8.0_231\jre\bin\java.exe
            # VM options: -ea
            # Warmup: 10 iterations, 10 s each
            # Measurement: 10 iterations, 10 s each
            # Timeout: 10 min per iteration
            # Threads: 1 thread, will synchronize iterations
            # Benchmark mode: Throughput, ops/time
            Benchmark                          Mode  Cnt      Score     Error   Units
            BeanCopyTest.testApacheBeanUtils  thrpt   10      4.077 ±   0.046  ops/ms
            BeanCopyTest.testCglibBeanCopier  thrpt   10  12158.830 ± 112.239  ops/ms
            BeanCopyTest.testDeadCode         thrpt   10  12393.230 ± 219.693  ops/ms
            BeanCopyTest.testOrikaBeanCopy    thrpt   10   1424.492 ±  16.948  ops/ms
            BeanCopyTest.testSpringBeanUtils  thrpt   10     88.815 ±   1.235  ops/ms
            

            根据测试结果,对象拷贝速度方面:

            手动拷贝 > cglib beanCopier > orika mapper > spring beanUtils > apache commons beanUtils

            由于 apache commons beanUtils 和 spring beanUtils 使用了大量反射,所以速度较慢;

            cglib beanCopier 和 orika mapper 使用动态代理生成包含 setter/getter 的代码的代理类,不需要调用反射来赋值,所以,速度较快。cglib beanCopier 的速度和手动拷贝不相上下。

            orika mapper 是深度复制,需要额外处理对象类型的属性转换,也增加了部分开销。

            以上数据仅供参考。感谢阅读。

            2021-05-28 更改

            相关源码请移步: beanCopy-tool-demo

            评论
            添加红包

            请填写红包祝福语或标题

            红包个数最小为10个

            红包金额最低5元

            当前余额3.43前往充值 >
            需支付:10.00
            成就一亿技术人!
            领取后你会自动成为博主和红包主的粉丝 规则
            hope_wisdom
            发出的红包
            实付
            使用余额支付
            点击重新获取
            扫码支付
            钱包余额 0

            抵扣说明:

            1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
            2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

            余额充值