《深入理解Java虛擬機》- JVM是如何實現反射的

  • 2019 年 10 月 3 日
  • 筆記

Java反射學問很深,這裡就淺談吧。如果涉及到方法內聯,逃逸分析的話,我們就說說是什麼就好了。有興趣的可以去另外看看,我後面可能也會寫一下。(因為我也不會呀~)

一、Java反射是什麼?

反射的核心是JVM在運行時才動態載入類或調用方法/訪問屬性,它不需要事先(寫程式碼的時候或編譯期)知道運行對象是誰。

反射是由類開始的,從class對象中,我們可以獲得有關該類的全部成員的完整列表;可以找出該類的所有類型、類自身資訊。

二、反射的一些應用

1、java集成開發環境,每當我們敲入點號時,IDE便會根據點號前的內容,動態展示可以訪問的欄位和方法。

2、java調試器,它能夠在調試過程中枚舉某一對象所有欄位的值。

3、web開發中,我們經常接觸到各種配置的通用框架。為保證框架的可擴展性,他往往藉助java的反射機制。例如Spring框架的依賴反轉(IOC)便是依賴於反射機制。

三、Java反射的實現

       1. Java反射使用的api(列舉部分,具體在rt.jar包的java.lang.reflect.*)中

列舉Class.java中的一些方法。這些都很常用,比如在你嘗試編寫一個mvc框架的時候,就可以參照這個類裡面的方法,再結合一些Servlet的api就實現一個簡單的框架。

 

     2.程式碼實現

       2.1程式碼實現的目的:說明反射調用是有兩種方式,一種是本地實現,另一種是委派實現。

這裡圍繞Method.invoke方法展開。查看invoke()源碼:

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

說明:invoke()是有MethodAccessor介面實現的,這個介面有倆實現:

一個是使用委派模式的“委派實現”,一個是通過本地方法調用來實現反射調用的“本地實現”。

這兩種實現不是獨立的,而是相互協作的。下面,用程式碼讓大家看一下具體操作。

Java程式碼:

public class InvokeDemo {      public static void target(int i){          new Exception("#"+i).printStackTrace();      }      public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {          Class<?> invokeDemo1 = Class.forName("com.example.demo.invoke_demo.InvokeDemo");          Method method1 = invokeDemo1.getMethod("target", int.class);          method1.invoke(null,0);      }  }

運行之後,便可以在異常棧中查找方法調用的路線:

java.lang.Exception: #0      at com.example.demo.invoke_demo.InvokeDemo.target(InvokeDemo.java:9)      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.example.demo.invoke_demo.InvokeDemo.main(InvokeDemo.java:15)

這裡,我們會看到,invoke方法是先調用委派實現,然後再將請求傳到本地方法實現的,最後在傳到目標方法使用。

為什麼要這樣做呢?為什麼不直接調用本地方法呢?

其實,Java的反射調用機制還設立了另一種動態生成位元組碼的實現(“動態實現”),直接使用invoke指令來調用目標方法。之所以採用委派實現,便是為了能夠在“本地實現”和動態實現之間來回切換。(但是,動態實現貌似並沒有開源

動態實現與本地實現的區別在於,反射程式碼段重複運行15次以上就會使用動態實現,15次以下就使用本地實現。下面是重複這個程式碼的控制台輸出的第#14、#15、#16段異常:

Class<?> invokeDemo1 = Class.forName("com.example.demo.invoke_demo.InvokeDemo");          Method method1 = invokeDemo1.getMethod("target", int.class);          method1.invoke(null,0);

控制台:

java.lang.Exception: #15      at com.example.demo.invoke_demo.InvokeDemo.target(InvokeDemo.java:9)      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.example.demo.invoke_demo.InvokeDemo.main(InvokeDemo.java:20)  java.lang.Exception: #16      at com.example.demo.invoke_demo.InvokeDemo.target(InvokeDemo.java:9)      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.example.demo.invoke_demo.InvokeDemo.main(InvokeDemo.java:20)  java.lang.Exception: #17      at com.example.demo.invoke_demo.InvokeDemo.target(InvokeDemo.java:9)      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.example.demo.invoke_demo.InvokeDemo.main(InvokeDemo.java:20)

從#15到#16異常鏈路看,反射的調用就開始從本地實現向動態實現的轉變。這 是JVM對反射調用進行辨別優化性能的一個手段。

另外注意一點,粉紅色部分的字體,標記為“unkown source” ,那就是不開源的吧,所以看不到那是啥。。

 

四、Java反射的性能開銷

public class InvokeDemo {      private static long n = 0;      public static void target(int i){          n++;      }      /* 8662ms      public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {          Class<?> invokeDemo1 = Class.forName("com.example.demo.invoke_demo.InvokeDemo");          Method method1 = invokeDemo1.getMethod("target", int.class);            long start = System.currentTimeMillis();          for (int i = 0; i < 1000000000; i++) {                  if(i==1000000000-1){                      long  total = System.currentTimeMillis()-start;                      System.out.println(total);                  }              method1.invoke(null,1);          }      }      */      // 161ms      public static void main(String[] args) {          long start = System.currentTimeMillis();          for (int i = 0; i < 1000000000; i++) {              if(i==1000000000-1){                  long  total = System.currentTimeMillis()-start;                  System.out.println(total);              }              target(1);          }      }  }

上面展示了使用反射調用和不使用反射調用的性能,結果表示,使用反射的耗時為8662ms,而不使用反射的耗時為161ms。這裡就可以看到差異。

那麼從位元組碼層面查看,又是什麼樣的一種風景呢?

 1.不使用反射:

public static void main(java.lang.String[]);      descriptor: ([Ljava/lang/String;)V      flags: ACC_PUBLIC, ACC_STATIC      Code:        stack=4, locals=6, args_size=1           0: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J           3: lstore_1           4: iconst_0           5: istore_3           6: iload_3           7: ldc           #4                  // int 1000000000           9: if_icmpge     43          12: iload_3          13: ldc           #5                  // int 999999999          15: if_icmpne     33          18: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J          21: lload_1          22: lsub          23: lstore        4          25: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;          28: lload         4          30: invokevirtual #7                  // Method java/io/PrintStream.println:(J)V          33: iconst_1          34: invokestatic  #8                  // Method target:(I)V          37: iinc          3, 1          40: goto          6          43: return        LineNumberTable:          line 8: 0          line 9: 4          line 10: 12          line 11: 18          line 12: 25          line 14: 33          line 9: 37          line 16: 43        StackMapTable: number_of_entries = 3          frame_type = 253 /* append */            offset_delta = 6            locals = [ long, int ]          frame_type = 26 /* same */          frame_type = 250 /* chop */            offset_delta = 9

 

2.使用反射:

 public static void main(java.lang.String[]) throws java.lang.ClassNotFoundException, java.lang.NoSuchMethodException, java.lang.reflect.InvocationTargetException, java.lang.IllegalAccessException;      descriptor: ([Ljava/lang/String;)V      flags: ACC_PUBLIC, ACC_STATIC      Code:        stack=6, locals=8, args_size=1           0: ldc           #3                  // String InvokeDemo2           2: invokestatic  #4                  // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;           5: astore_1           6: aload_1           7: ldc           #5                  // String target           9: iconst_1          10: anewarray     #6                  // class java/lang/Class          13: dup          14: iconst_0          15: getstatic     #7                  // Field java/lang/Integer.TYPE:Ljava/lang/Class;          18: aastore          19: invokevirtual #8                  // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;          22: astore_2          23: invokestatic  #9                  // Method java/lang/System.currentTimeMillis:()J          26: lstore_3          27: iconst_0          28: istore        5          30: iload         5          32: ldc           #10                 // int 1000000000          34: if_icmpge     82          37: iload         5          39: ldc           #11                 // int 999999999          41: if_icmpne     59          44: invokestatic  #9                  // Method java/lang/System.currentTimeMillis:()J          47: lload_3          48: lsub          49: lstore        6          51: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;          54: lload         6          56: invokevirtual #13                 // Method java/io/PrintStream.println:(J)V          59: aload_2          60: aconst_null          61: iconst_1          62: anewarray     #14                 // class java/lang/Object          65: dup          66: iconst_0          67: iconst_1          68: invokestatic  #15                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;          71: aastore          72: invokevirtual #16                 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;          75: pop          76: iinc          5, 1          79: goto          30          82: return

anewarray: 表示創建一個引用類型的(如類、介面、數組)數組,並將其引用值壓如棧頂 (1: anewarray #2)

 

大致的分析:

1.綠色部分:反射調用分配了更多的棧,說明需要進行比普通調用還要多的棧空間分配,也就是pop出,push進。。

2.從方法體上看: 在反射部分程式碼中的藍色背景部分,也就是62行位元組碼,使用了創建數組這一操作,並且還有68行的將int類型的1進行裝箱操作,這些步驟對於普通調用來說,都是多出來的,自然也就比普通調用的方式耗時得多了。

 

但是,普通調用和反射調用一個方法的用途不一樣,我們不能為了反射調用而調用,最好能夠在普通調用無法滿足的情況下進行該操作。

 

//五、優化反射調用