1. 程式人生 > >07 JVM 是如何實現反射的

07 JVM 是如何實現反射的

進入 技術分享 以及 通用 虛擬機設置 com ray 文件加載 另一個

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 虛擬機》課程,通過課後反思以及借鑒各位學友的發言總結,現整理出自己的知識架構,以便日後溫故知新,查漏補缺。

關註本人公眾號,第一時間獲取最新文章發布,每日更新一篇技術文章。

技術分享圖片

07 JVM 是如何實現反射的