Java反射在JVM的實現
1. 什麼是Java反射,有什麼用?
反射使程式程式碼能夠接入裝載到JVM中的類的內部資訊,允許在編寫與執行時,而不是原始碼中選定的類協作的程式碼,是以開發效率換執行效率的一種手段。這使反射成為構建靈活應用的主要工具。
反射可以:
- 呼叫一些私有方法,實現黑科技。比如雙卡簡訊傳送、設定狀態列顏色、自動掛電話等。
- 實現序列化與反序列化,比如PO的ORM,Json解析等。
- 實現跨平臺相容,比如JDK中的SocketImpl的實現
- 通過xml或註解,實現依賴注入(DI),註解處理,動態代理,單元測試等功能。比如Retrofit、Spring或者Dagger
2. Java Class檔案的結構
在*.class檔案中,以Byte流的形式進行Class的儲存,通過一系列Load,Parse後,Java程式碼實際上可以對映為下圖的結構體,這裡可以用javap
命令或者IDE外掛進行檢視。
typedef struct { u4 magic;/*0xCAFEBABE*/ u2 minor_version; /*網上有表可查*/ u2 major_version; /*網上有表可查*/ u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; //重要 u2 fields_count; field_info fields[fields_count]; //重要 u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }ClassBlock;
- 常量池(constant pool):類似於C中的DATA段與BSS段,提供常量、字串、方法名等值或者符號(可以看作偏移定值的指標)的存放
- access_flags: 對Class的flag修飾
typedef enum { ACC_PUBLIC = 0x0001, ACC_FINAL = 0x0010, ACC_SUPER = 0x0020, ACC_INTERFACE = 0x0200, ACC_ACSTRACT = 0x0400 }AccessFlag
- this class/super class/interface: 一個長度為u2的指標,指向常量池中真正的地址,將在Link階段進行符號解引。
- filed: 欄位資訊,結構體如下
typedef struct fieldblock { char *name; char *type; char *signature; u2 access_flags; u2 constant; union { union { char data[8]; uintptr_t u; long long l; void *p; int i; } static_value; u4 offset; } u; } FieldBlock;
method: 提供descriptor, access_flags, Code等索引,並指向常量池:
它的結構體如下,詳細在這裡
method_info { u2 access_flags; u2 name_index; //the parameters that the method takes and the //value that it return u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
以上具體內容可以參考
3. Java Class載入的過程
Class的載入主要分為兩步
- 第一步通過ClassLoader進行讀取、連結操作
- 第二步進行Class的
<clinit>()
初始化。
3.1. Classloader載入過程
ClassLoader用於載入、連線、快取Class,可以通過純Java或者native進行實現。在JVM的native程式碼中,ClassLoader內部維護著一個執行緒安全的HashTable<String,Class>
,用於實現對Class位元組流解碼後的快取,如果HashTable中已經有了快取,則直接返回快取;反之,在獲得類名後,通過讀取檔案、網路上的class位元組流反序列化為JVM中native的C結構體,接著malloc記憶體,並將指標快取在HashTable中。
下面是非陣列情況下ClassLoader的流程
- find/load: 將檔案反序列化為C結構體。
- link: 根據Class結構體常量池進行符號的解引。比如物件計算記憶體空間,建立方法表,native invoker,介面方法表,finalizer函式等工作。
3.2. 初始化過程
當ClassLoader載入Class結束後,將進行Class的初始化操作。主要執行<clinit()>
的靜態程式碼段與靜態變數(取決於原始碼順序)。
public class Sample { //step.1 static int b = 2; //step.2 static { b = 3; } public static void main(String[] args) { Sample s = new Sample(); System.out.println(s.b); //b=3 } }
具體參考如下:
在完成初始化後,就是Object的構造<init>
了,本文暫不討論。
4. 反射在native的實現
反射在Java中可以直接呼叫,不過最終呼叫的仍是native方法,以下為主流反射操作的實現。
4.1. Class.forName的實現
Class.forName可以通過包名尋找Class物件,比如Class.forName("java.lang.String")
。
在JDK的原始碼實現中,可以發現最終呼叫的是native方法forName0()
,它在JVM中呼叫的實際是findClassFromClassLoader()
,原理與ClassLoader的流程一樣,具體實現已經在上面介紹過了。
4.2. getDeclaredFields的實現
在JDK原始碼中,可以知道class.getDeclaredFields()
方法實際呼叫的是native方法getDeclaredFields0()
,它在JVM主要實現步驟如下
- 根據Class結構體資訊,獲取
field_count
與fields[]
欄位,這個欄位早已在load過程中被放入了 - 根據
field_count
的大小分配記憶體、建立陣列 - 將陣列進行forEach迴圈,通過
fields[]
中的資訊依次建立Object物件 - 返回陣列指標
主要慢在如下方面
- 建立、計算、分配陣列物件
- 對欄位進行迴圈賦值
4.3. Method.invoke的實現
以下為無同步、無異常的情況下呼叫的步驟
- 建立Frame
- 如果物件flag為native,交給native_handler進行處理
- 在frame中執行java程式碼
- 彈出Frame
- 返回執行結果的指標
主要慢在如下方面
- 需要完全執行ByteCode而缺少JIT等優化
- 檢查引數非常多,這些本來可以在編譯器或者載入時完成
4.4. class.newInstance的實現
- 檢測許可權、預分配空間大小等引數
- 建立Object物件,並分配空間
- 通過Method.invoke呼叫建構函式(
<init>()
) - 返回Object指標
主要慢在如下方面
- 引數檢查不能優化或者遺漏
<init>()
的查表- Method.invoke本身耗時
5. 附錄
5.1. JVM與原始碼閱讀工具的選擇
初次學習JVM時,不建議去看Android Art、Hotspot等重量級JVM的實現,它內部的防禦程式碼很多,還有android與libcore、bionic庫緊密耦合,以及分層、內聯甚至能把編譯器的語義分析繞進去,因此找一個教學用的、嵌入式小型的JVM有利於節約自己的時間。因為以前折騰過OpenWrt,聽過有大神推薦過jamvm,只有不到200個原始檔,非常適合學習。
在工具的選擇上,個人推薦SourceInsight。對比了好幾個工具clion,vscode,sublime,sourceinsight,只有sourceinsight對索引、符號表的解析最準確。
5.2. 關於幾個ClassLoader
參考這裡
ClassLoader0:native的classloader,在JVM中用C寫的,用於載入rt.jar的包,在Java中為空引用。
ExtClassLoader: 用於載入JDK中額外的包,一般不怎麼用
AppClassLoader: 載入自己寫的或者引用的第三方包,這個最常見
例子如下
//sun.misc.Launcher$AppClassLoader@4b67cf4d //which class you create or jars from thirdParty //第一個非常有歧義,但是它的確是AppClassLoader ClassLoader.getSystemClassLoader(); com.test.App.getClass().getClassLoader(); Class.forName("ccom.test.App").getClassLoader() //sun.misc.Launcher$ExtClassLoader@66d3c617 //Class loaded in ext jar Class.forName("sun.net.spi.nameservice.dns.DNSNameService") //null, class loaded in rt.jar String.class.getClassLoader() Class.forName("java.lang.String").getClassLoader() Class.forName("java.lang.Class").getClassLoader() Class.forName("apple.launcher.JavaAppLauncher").getClassLoader()
最後就是getContextClassLoader()
,它在Tomcat中使用,通過設定一個臨時變數,可以向子類ClassLoader去載入,而不是委託給ParentClassLoader
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); // call some API that uses reflection without taking ClassLoader param } finally { Thread.currentThread().setContextClassLoader(originalClassLoader); }
最後還有一些自定義的ClassLoader,實現加密、壓縮、熱部署等功能,這個是大坑,晚點再開。
5.3. 反射是否慢?
在Stackoverflow上認為反射比較慢的程式設計師主要有如下看法
- 驗證等防禦程式碼過於繁瑣,這一步本來在link階段,現在卻在計算時進行驗證
- 產生很多臨時物件,造成GC與計算時間消耗
- 由於缺少上下文,丟失了很多執行時的優化,比如JIT(它可以看作JVM的重要評測標準之一)
當然,現代JVM也不是非常慢了,它能夠對反射程式碼進行快取以及通過方法計數器同樣實現JIT優化,所以反射不一定慢。
更重要的是,很多情況下,你自己的程式碼才是限制程式的瓶頸。因此,在開發效率遠大於執行效率的的基礎上,大膽使用反射,放心開發吧。
參考文獻
- http://www.codeceo.com/article/reflect-bad.html
- http://blog.csdn.net/lmj623565791/article/details/43452969
- http://codekk.com/open-source-project-analysis/detail/Android/Trinea/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7%82%B9%E4%B9%8BJava%20%E6%B3%A8%E8%A7%A3%20Annotation
- http://www.trinea.cn/android/java-annotation-android-open-source-analysis/
相關文章
- java實現連結串列反轉Java
- JVM系列(四):java方法的查詢過程實現JVMJava
- Java無所不能的反射在Android中需要熟練的知識Java反射Android
- JVM是如何實現反射的JVM反射
- 詳解 LeetCode_007_整數反轉(Java 實現)LeetCodeJava
- C++ 靜態反射在網易雲信 SDK 中的實踐C++反射
- Java致命傷:持續交付使JVM JIT成為反模式 - astradotJavaJVM模式AST
- JVM的ServerSocket是怎麼實現的(上)JVMServer
- JVM的ServerSocket是怎麼實現的(下)JVMServer
- jvm(三)——jvm垃圾回收演算法以及實現JVM演算法
- Java當中的JVMJavaJVM
- 用Unity實現彈反效果Unity
- .NETCore中實現ObjectId反解NetCoreObject
- 深入談談String.intern()在JVM的實現JVM
- JVM系列(五):gc實現概要01JVMGC
- JVM系列.歷史上出現過的Java虛擬機器JVMJava虛擬機
- TypeScript 實現連結串列反轉TypeScript
- React實現全選和反選React
- 《深入Java虛擬機器:JVM G1GC的演算法與實現》中的網址Java虛擬機JVMGC演算法
- 記憶體屏障在CPU、JVM、JDK中的實現記憶體JVMJDK
- PHP與反ajax推送,實現的訊息實時推送功能PHP
- 程式碼回現 | 如何實現交易反欺詐?
- 溪源的Java筆記—JVMJava筆記JVM
- java之JVM的架構模型JavaJVM架構模型
- 【Java】JVM複習JavaJVM
- JAVA 小白搞死JVMJavaJVM
- echarts圖表y軸資料反轉的實現Echarts
- iOS實現反轉二叉樹iOS二叉樹
- 支付寶截圖反饋功能實現
- 向JVM註冊本地方法是怎麼實現的JVM
- 深入解析多型和方法呼叫在JVM中的實現多型JVM
- Java ArrayList.add 的實現Java
- Java中HashMap的實現原理JavaHashMap
- Java實現的攔截器Java
- Java中鎖的實現方式Java
- Java 中的物件池實現Java物件
- Java 物件實現 Serializable 的原因Java物件
- java實現簡單的JDBCJavaJDBC
- Java 實現的SnowFlake生成UUID (Java程式碼實戰-007)JavaUI