jvm执行反射

版权声明: https://blog.csdn.net/qq_18298439/article/details/84632712

        反射是java中一个相当重要的特性,它的应用十分广泛。譬如java调试器,在调试过程中枚举对象所有字段的值。在Web开发中,各种可配置的框架。为了框架的扩展性,基本上都是使用反射机制。譬如Spring的IOC容器。当然,这么方便的东西往往是牺牲另一部分的特性锁带来的,而反射牺牲的则是代码执行的性能。下面就来简单分析下反射的机制

反射的实现
首先看下Method类的源码

public final class Method extends Executable {   //java8
 
    @CallerSensitive
    public Object invoke(Object var1, Object... var2) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        if (!this.override && !Reflection.quickCheckMemberAccess(this.clazz, this.modifiers)) {
            Class var3 = Reflection.getCallerClass();
            this.checkAccess(var3, this.clazz, var1, this.modifiers);
        }
 
        MethodAccessor var4 = this.methodAccessor;
        if (var4 == null) {
            var4 = this.acquireMethodAccessor();
        }
 
        return var4.invoke(var1, var2);
    }


省略了其他的东西,只看invoke方法。可以看到,方法具体的实现是委托给了MethodAccessor。MethodAccessor是一个接口,有三个子类,子类中有一个抽象类MethodAccessorImpl,两个具体实现DelegatingMethodAccessorImpl和NativeMethodAccessorImpl。在Method实例化的时候,就会生成一个委托,默认的委托则是NativeMethodAccessorImpl,即本地方法实现。在东西很容易理解,在进入jvm之后,就可以得到这个本地方法的地址,然后把参数传过去直接调用你就行了。

贴个代码   

public class ReflectTest {
 
 
    public static void test(int i) {
        new Exception(i + "").printStackTrace();
    }
 
    public static void main(String[] args) throws Exception {
        Class<?> aClass = Class.forName("ReflectTest");
        Method test = aClass.getMethod("test", int.class);
        test.invoke(null, 1);
    }
}
 


--------------输出

java.lang.Exception: 1
    at com.lv.ddpay.test.ReflectTest.test(ReflectTest.java:13)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.lv.ddpay.test.ReflectTest.main(ReflectTest.java:19)


在test方法打印了一个调用栈,可以看到,首先进入Method.invoke方法,然后进入委托,委托在调用本地方法实现,然后在进入具体的方法。
在这个流程中,有疑问的应该就是这个委托了,问什么要用这个委托呢,直接使用本地方法不是更快吗?那是因为jvm还有一种反射实现,是直接生产字节码,使用invoke指令直接调用的方式。之所以使用委托,是为了在本地方法和动态字节码的方式之间做切换。下面,在text.invoke上加个循环

public class ReflectTest {
 
 
    public static void test(int i) {
        new Exception(i + "").printStackTrace();
    }
 
    public static void main(String[] args) throws Exception {
        Class<?> aClass = Class.forName("com.lv.ddpay.test.ReflectTest");
        Method test = aClass.getMethod("test", int.class);
        for (int i = 0; i < 20; i++) {
            long l = System.currentTimeMillis();
            test.invoke(null, i);
            System.out.println(System.currentTimeMillis() - l );
            
        }
    }
}


------输出-------java8

java.lang.Exception: 15
    at com.lv.ddpay.test.ReflectTest.test(ReflectTest.java:13)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.lv.ddpay.test.ReflectTest.main(ReflectTest.java:21)
java.lang.Exception: 16
    at com.lv.ddpay.test.ReflectTest.test(ReflectTest.java:13)
    at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.lv.ddpay.test.ReflectTest.main(ReflectTest.java:21)


       从输出可以看到,java8在第17次输出的时候,方法调用栈改变了使用的是DelegatingMethodAccessorImpl,然后在进入GeneratedMethodAccessor1,GeneratedMethodAccessor1就是动态生成的字节码。动态字节码比本地实现快了差不多20倍,因为这里不需要有java-C++-java的切换,但生成字节码比较耗时,如果仅调用一次的话,本地实现比动态字节码快。

       jvm有个参数-Dsun.reflect.inflationThresold? 来设置使用字节码的阈值。当达到了这个阈值的时候,jvm就生成字节码,采用动态调用。还有个参数-Dsun.reflect.noInflation 来设置当使用反射的时候,是不是直接使用动态字节码的方式。

反射的开销
       从刚才的代码来看,主要有三个操作Class.forName、class.getMethod、method.invoke。Class.forName调用的是本地方法,class.getMethod会遍历该类所有的公有方法,没找到还会遍历父类的公有方法。可想而知,这两个方法的调用开销都不小。

但在应用程序里,大部分使用Class.forName、class.getMethod的结果都会缓存下来,所以开销主要是看method.invoke方法。

      27: aload_2           //加载invoke方法             
      28: aconst_null        //第一个参数 null
      29: iconst_1
      30: anewarray     #18    //生成长度为1的object数组       // class java/lang/Object
      33: dup
      34: iconst_0
      35: iconst_1
      36: invokestatic  #19 
      //int参数自动装箱 
      // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      39: aastore           //把装箱后的参数放入数组
      40: invokevirtual #20 // Method java/lang/reflect/Method.i


nvoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
把上面的代码编译一下,在用javap解析一下字节码? ?method.invoke(null,1)编译后就得到上面的代码。可以看到,除了直接滴啊用invoke方法外,还有两个额外的操作,

invoke方法的参数是一个可变长度,在字节码层面上是一个Object数组,所以java编译器会在方法调用的地方生产一个Object数组,并把参数存储进该数组
Object数组不能存储基本类型,所以java编译器会对基本类型进行自动装箱处理。
 

猜你喜欢

转载自blog.csdn.net/qq_18298439/article/details/84632712