07 JVM 是如何實現反射的
Java 中的反射
反射是 Java 語言的一個相當重要的特性,它允許正在執行的 Java 程式觀測,甚至是修改程式的動態行為。
我們可以通過 Class 物件列舉該類中的所有方法,還可以通過 Method.SetAccessible 讓過 Java 語言的訪問許可權,在私有方法所在類之外的地方呼叫該方法。
反射在 Java 中的引用十分廣泛。日常我們用的 Java 繼承開發工具 IDE 便運用了這一功能。比如,敲下點號時,IDE便會根據點號之前的內容動態的展示可以訪問的欄位或者方法。Java 偵錯程式,能夠在除錯過程中列舉某一物件所有欄位的值。當然,這些功能的實現也用到了語法樹。在 Web 開發中,我們接觸到的各種通用框架。為了保證框架的可擴充套件性,旺旺藉助 Java 的反射機制,根據配置檔案載入不同的類。例如 Spring 框架的以來反轉 IOC。
反射呼叫的實現
首先,看一下 Method.invoke 的實現
public final class Method extends Executable { ... public Object invoke(Object obj, Object... args) throws ... { ... // 許可權檢查 MethodAccessor ma = methodAccessor; if (ma == null) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); } }
上面程式碼中,invoke 方法實際上委派給了 MethodAccessor 來處理。MethodAccessor 是一個介面,有兩個具體的實現。一個是通過本地方法來實現反射呼叫,另一個則使用了委派模式。
每個 Method 例項第一次反射呼叫都會生成一個委派實現,它所委派的具體實現便是一個本地實現。在反射過程中,反射呼叫先是呼叫了 Method.invoke,然後進入委派實現 DelegatingMethodAccessorlmpl,再然後進入本地實現 NativeMethodAccessorlmpl,最後到達目標方法。
有個疑問,為什麼反射呼叫還要採取委託實現作為中間層,為何不直接交給本地實現?
其實,Java 的反射呼叫機制還設立了另一種動態生成位元組碼的實現(下稱動態實現)來直接使用 invoke 指令來呼叫目標方法。之所以採用委派實現,是為了能夠在本地實現和動態實現中切換。動態實現和本地實現相比,執行效率更高。這是因為動態實現無需經過 Java 到 C++ 再到 Java 的切換,單由於生成位元組碼十分耗時,僅呼叫一次的話,反而是本地實現要更快。
實際中,許多反射呼叫僅會執行一次,Java 虛擬機器設定了一個閾值 15,當某個反射呼叫的呼叫次數在 15 之下時,採用本地實現;當達到 15 時,便開始動態生成位元組碼,並將委派實現的委派物件切換至動態實現,這個過程叫 inflation。
反射呼叫的 inflation 機制是可以通過引數(-Dsun.reflect.noinflation = true)來關閉的。這樣一來,在反射呼叫一開始便會直接生成動態實現,而不會使用委派實現或者本地實現。
反射呼叫的開銷
在反射中 Class.forName,Class.getMethod 以及 Method.invoke 這三個方法,Class.forName 會呼叫本地方法,Class.getMethod 會遍歷該類的共有方法。如果沒有匹配,則會遍歷父類的方法。在以 getMethod 為代表的查詢方法操作中,會返回查詢得到結果的一份拷貝。因此,應該避免在熱點程式碼中使用返回 Method 陣列的 getMethods 或者 getDeclaredMethods 方法,以減少不必要的堆空間消耗。實際開發中,我們也往往會快取 Class.forName,Class.getMethod 的結果,因此,下面只關注反射呼叫本身的效能開銷。
第一,Method.invoke 中的第二個引數是一個可以變長度的 Object 陣列,陣列中存放的都是物件型別。如果我們存入引數是基本型別,可以提前裝箱,減少效能損耗。
第二,可以關閉反射呼叫的 inflation 機制,從而取消委派實現,並且直接使用動態實現。
第三,每次反射呼叫都會檢查目標方法的許可權,而這個檢查同樣可以在 Java 程式碼裡關閉。
反射 API 簡介
使用反射 API 第一步便是獲取 Class 物件。獲取物件有以下三種方式:
1:使用靜態方法 Class.forName 來獲取
2:呼叫物件的 getClass 方法
3:直接使用 類名+“.class” 訪問。對於基本型別來說,他們的包裝型別擁有一個名為 "TYPE" 的 final 靜態欄位,指向該基本型別對應的 Class 物件。
例如,Integer.TYPE 指向 int.class。對於陣列型別來說,可以使用 類名+[].class 來訪問,如 int[].class。
除此之外,Class 類和 java.lang.reflect 包中還提供了許多返回 Class 物件的方法。
獲得 Class 物件之後,就可以正式使用反射功能了。下列為常用的幾項:
1:使用 newInstance() 生成一個該類的例項。該類必須有一個無參建構函式
。
2:使用 isInstance(Object) 判斷一個物件是否是該類的例項,語法上等同於 instanceof。
3:使用 Array.newInstance(Class,int) 來構造該型別的陣列。
4:使用 getFields()/getConstructors()/getMethods() 來訪問該類的成員。
問答
Q:當某個反射呼叫的呼叫次數在 15 之下時,採用本地實現;當達到 15 時,便開始動態生成位元組碼...
動態生成發生在第15次(從0開始數的話),所以第15次比較耗時。
Q:什麼是 inflation 機制
反射的inflation機制是當反射被頻繁呼叫時,動態生成一個類來做直接呼叫的機制,可以加速反射呼叫
總結
本文創作靈感來源於 極客時間 鄭雨迪老師的《深入拆解 Java 虛擬機器》課程,通過課後反思以及借鑑各位學友的發言總結,現整理出自己的知識架構,以便日後溫故知新,查漏補缺。
關注本人公眾號,第一時間獲取最新文章釋出,每日更新一篇技術文章。