• Java反射的性能问题


    前言

    动态代理分为两种,JDK动态代理和spring里边使用的Cglib动态代理。分别使用的是interface和子类继承的思路来对委托类进行wrap生成代理类。

    本篇算是动态代理系列的番外篇(前文:https://www.cnblogs.com/lyhero11/p/15557389.html)
    一直据说由于JDK动态代理使用的是反射的方式对委托类的方法进行调用,性能低,而cglib使用的是字节码修改的方式,性能高。
    本篇就尝试搞清楚低为什么低,而高为什么高。

    以下分析环境所用的jdk版本:

    java version "1.8.0_202"
    Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
    Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
    

    java反射,1、动态化的调用使得JIT编译优化没法做 2、newInstance创建Object,getDeclareMethod,Method.invoke() 耗时

    ASM,Cglib可以直接生成class文件或在class load之前修改class文件

    修改class文件 -> 生成$Proxy类 -> load到jvm,这样一个过程,所以第一次会慢一些,但一旦载入jvm之后,就跟普通的Java类一样了,对象的方法调用也是可以被JIT优化的了。

    避免大量循环使用反射调用,但如果跟JDBC这种SQL调用一起,那么反射的性能损耗基本可以忽略不记了。

    比较Java反射与普通对象方法调用的性能

    我们用一个例子来比较一下普通对象方法调用、java反射、基于字节码修改的reflectAsm反射,这几种方法调用方式的性能差别。

    import com.esotericsoftware.reflectasm.MethodAccess;
    import lombok.extern.slf4j.Slf4j;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    /**
     * java反射性能测试
     * */
    @Slf4j
    public class ReflectTest {
    
        public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
            long start , end;
            int tenMillion = 10000000;
    
            //1、普通new对象,调用方法
            DummyObject obj = new DummyObject();
            start = System.currentTimeMillis();
            for(int i=0; i<tenMillion; i++){
                obj.setValue(i);
            }
            log.info("普通对象方法调用耗时{}ms" , System.currentTimeMillis() - start);
            log.info("value = {}", obj.getValue());
    
            //2、使用反射,method.invoke调用方法
            Class clazz = Class.forName("com.wangan.springbootone.aop.ReflectTest$DummyObject");
            Class[] argsType = new Class[1];
            argsType[0] = int.class;
            Method method = clazz.getDeclaredMethod("setValue", argsType);
            DummyObject dummyObject = (DummyObject) clazz.newInstance();
            start = System.currentTimeMillis();
            for(int i=0; i<tenMillion; i++){
                method.invoke(dummyObject, i);
            }
            log.info("反射方法invoke调用耗时{}ms" , System.currentTimeMillis() - start);
            log.info("value = {}", dummyObject.getValue());
    
            //3、反射调用,getDeclaredMethod + invoke耗时
            start = System.currentTimeMillis();
            for(int i=0; i<tenMillion; i++){
                method = clazz.getDeclaredMethod("setValue", argsType); //比较耗时
                method.invoke(dummyObject, i);
            }
            log.info("反射方法getDeclaredMethod + invoke调用耗时{}ms" , System.currentTimeMillis() - start);
            log.info("value = {}", dummyObject.getValue());
    
            //4、使用reflectAsm高性能反射库invoke调用
            MethodAccess methodAccess = MethodAccess.get(DummyObject.class);
            int index = methodAccess.getIndex("setValue");
            start = System.currentTimeMillis();
            for(int i=0; i<tenMillion; i++){
                methodAccess.invoke(dummyObject, index, i);
            }
            log.info("使用reflectasm的invoke调用耗时{}ms" , System.currentTimeMillis() - start);
            log.info("value = {}", dummyObject.getValue());
        }
    
        public static class DummyObject{
            private int value;
    
            public void setValue(int v){
                value = v;
            }
            public int getValue(){
                return value;
            }
        }
    }
    

    输出:

    11:35:58.720 [main] INFO com.wangan.springbootone.aop.ReflectTest - 普通对象方法调用耗时4ms
    11:35:58.787 [main] INFO com.wangan.springbootone.aop.ReflectTest - 反射方法invoke调用耗时62ms
    11:35:59.913 [main] INFO com.wangan.springbootone.aop.ReflectTest - 反射方法getDeclaredMethod + invoke调用耗时1126ms
    11:35:59.991 [main] INFO com.wangan.springbootone.aop.ReflectTest - 使用reflectasm的invoke调用耗时61ms
    

    对一个DummyObject的set方法调用1千万次,普通方法耗时仅4ms,java反射方法只method.invoke的话是62ms,使用reflectasm的invoke耗时接近、61ms, 最慢的是java反射class.getDeclaredMethod + method.invoke、需要1126ms。

    我们可以得出几个阶段性结论:

    1、普通对象方法调用最快
    2、如果仅测试method.invoke的话,那么java自己的反射方法调用跟reflectasm的invoke性能差不多
    3、所谓java反射性能不行,实际上我们看是慢在getDeclaredMethod上,也就是根据Class对象到方法区里边查找类的方法定义的过程,找到方法定义之后真正method.invoke方法调用其实不算很慢。
    4、getDeclaredMethod非常慢、差300倍了,method.invoke跟普通的对象方法调用相比也慢了10几倍差1个数量级的样子。

    尝试分析一波原因

    Class.getDeclaredMethod方法

    @CallerSensitive
    public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        //接入校验
        checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
        //方法查找
        Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
        if (method == null) {
            throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
        }
        return method;
    }
    

    checkMemberAccess校验方法是否允许调用,可见性检查。

    privateGetDeclaredMethods方法查找先尝试取缓存,没找到就调用getDeclaredMethods0这个native方法,request value from VM 。使用缓存,这个JNI调用是个相对耗时的操作。

    Method.invoke方法:

    @CallerSensitive
    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) { //参数校验
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }
    

    MethodAccessor的实现有java版本和native版本

    public MethodAccessor newMethodAccessor(Method var1) {
        checkInitted();
        if (noInflation && !ReflectUtil.isVMAnonymousClass(var1.getDeclaringClass())) {
            // 这里返回的是MethodAccessorImpl
            return (new MethodAccessorGenerator()).generateMethod(var1.getDeclaringClass(), var1.getName(), var1.getParameterTypes(), var1.getReturnType(), var1.getExceptionTypes(), var1.getModifiers());
        } else {
            //否则使用NativeMethodAccessorImpl
            NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
            DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
            var2.setParent(var3);
            return var3;
        }
    }
    
    inflationThreshold = 15; //反射调用超过这个次数则使用MethodAccessorImpl,否则默认使用NativeMethodAccessorImpl
    

    Java 反射效率低主要原因是:

    1. Method#invoke 方法会对参数做封装和解封操作
    2. 需要检查方法可见性
    3. 需要校验参数
    4. 反射方法难以内联
    5. JIT 无法优化
    6. 请求jvm去查找其方法区中的方法定义,需要使用jni、开销相对比较大。

    所以cglib使用了FastClass机制来索引类的方法调用。也能实现Java反射的"运行时动态方法调用"的功能。

    参考:

    java反射的性能问题 - 王 庆 - 博客园 (cnblogs.com)

    都说 Java 反射效率低,究竟原因在哪里? - 知乎 (zhihu.com)

    JAVA深入研究——Method的Invoke方法。 - 寂静沙滩 - 博客园 (cnblogs.com)

  • 相关阅读:
    浅谈Java中的深拷贝和浅拷贝(转载)
    浅析Java中的final关键字
    Java内部类详解
    那些年震撼我们心灵的音乐
    深入理解Java的接口和抽象类
    Java:类与继承
    Java中的static关键字解析
    Java垃圾回收机制
    java 字节流和字符流的区别 转载
    Java 输入输出流 转载
  • 原文地址:https://www.cnblogs.com/lyhero11/p/15558956.html
Copyright © 2020-2023  润新知