《深入理解Java虛擬機器》- JVM是如何實現反射的
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進行裝箱操作,這些步驟對於普通呼叫來說,都是多出來的,自然也就比普通呼叫的方式耗時得多了。
但是,普通呼叫和反射呼叫一個方法的用途不一樣,我們不能為了反射呼叫而呼叫,最好能夠在普通呼叫無法滿足的情況下進行該操作。
五、優化反射呼叫
(明天再寫吧。。。demo都沒寫出來,不好意思寫了。。)
&n