1. 程式人生 > >jvm執行反射

jvm執行反射

        反射是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編譯器會對基本型別進行自動裝箱處理。