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)

posted on 2021-11-15 22:45  肥兔子爱豆畜子  阅读(1119)  评论(0编辑  收藏  举报

导航