天天看點

JVM 7:方法的調用

方法的調用

      • 1.方法調用的位元組碼指令
      • 2.非虛方法
      • 3.虛方法
        • 3.1分派
          • 3.1.1靜态分派
          • 3.1.2動态分派
        • 3.2接口調用
        • 3.3Lambda 表達式
          • 3.3.1invokedynamic
          • 3.3.2方法句柄(MethodHandle)
          • 3.3.3Lambda 表達式的捕獲與非捕獲

1.方法調用的位元組碼指令

關于方法的調用,Java 位元組碼共提供了 5 個指令,來調用不同類型的方法:

  • invokestatic 用來調用靜态方法;
  • invokespecial 用于調用私有執行個體方法、構造器及 super 關鍵字等;
  • invokevirtual 用于調用非私有執行個體方法,比如 public 和 protected,大多數方法調用屬于這一種;
  • invokeinterface 和上面這條指令類似,不過作用于接口類;
  • invokedynamic 用于調用動态方法。

2.非虛方法

  • 如果方法在編譯期就确定了具體的調用版本,這個版本在運作時是不可變的,這樣的方法稱為非虛方法。
  • 隻要能被 invokestatic 和 invokespecial 指令調用的方法,都可以在解析階段中确定唯一的調用版本,Java 語言裡符合這個條件的方法共有靜态方法、私有 方法、執行個體構造器、父類方法 4 種,再加上被 final 修飾的方法(盡管它使用 invokevirtual 指令調用),這 5 種方法調用會在類加載的時候就可以把符号引用 解析為該方法的直接引用。不需要在運作時再去完成。
  • Class檔案檢視工具
  1. javap 是 JDK 自帶的反解析工具。它的作用是将 .class 位元組碼檔案解析成可讀的檔案格式。 在使用 javap 時添加 -v 參數,盡量多列印一些資訊。同時,使用 -p 參數,列印一些私有的字段和方法。
  2. jclasslib 是一個圖形化的工具,能夠更加直覺的檢視位元組碼中的内容。它還分門别類的對類中的各個部分進行了整理,非常的人性化。同時,它還提供了 Idea 的插件,可以從 plugins 中搜尋到它。 jclasslib 的下載下傳位址:https://github.com/ingokegel/jclasslib
  • invokestatic 用來調用靜态方法;
    JVM 7:方法的調用
    JVM 7:方法的調用
  • 這個方法調用在編譯期間就明确以常量池項的形式固化在位元組碼指令的參數之中了。
    JVM 7:方法的調用
  • invokespecial 用于調用私有執行個體方法、構造器及 super 關鍵字等;
    JVM 7:方法的調用

3.虛方法

  • 與非虛方法相反,不是虛方法的方法就是虛方法。主要包括以下位元組碼中的兩類
  1. invokevirtual 用于調用非私有執行個體方法,比如 public 和 protected,大多數方法調用屬于這一種(排除掉被 final 修飾的方法);
  2. invokeinterface 和上面這條指令類似,不過作用于接口類;
  • 為什麼叫做虛方法呢?就是方法在運作時是可變的。
  • 很多時候,JVM 需要根據調用者的動态類型,來确定調用的目标方法,這就是動态綁定的過程;相對比,invokestatic 指令加上 invokespecial 指令,就屬于靜态綁定過程。
  • 因為 invokeinterface 指令跟 invokevirtual 類似,隻是作用與接口,是以我們隻要熟悉 invokevirtual 即可。

3.1分派

  • 要了解虛方法必須了解以下基礎:
  • Java 是一門面向對象的程式語言,因為 Java 具備面向對象的 3 個基本特征:繼承、封裝和多态。
  • 分派調用過程将會揭示多态性特征的一些最基本的展現,如“重載”和“重寫”在 Java 虛拟機之中是如何實作的
3.1.1靜态分派
  • 多見于方法的重載。(重載:一個類中允許同時存在一個以上的同名方法,這些方法的參數個數或者類型不同)
    JVM 7:方法的調用
  • “Human”稱為變量的靜态類型(Static Type),或者叫做的外觀類型(Apparent Type),後面的“Man”則稱為變量的實際類型(Actual Type)。 靜态類型和實際類型在程式中都可以發生一些變化,差別是靜态類型的變化僅僅在使用時發生,變量本身的靜态類型不會被改變,并且最終的靜态類型 是在編譯期可知的;而實際類型變化的結果在運作期才可确定,編譯器在編譯程式的時候并不知道一個對象的實際類型是什麼。
  • 代碼中定義了兩個靜态類型相同但實際類型不同的變量,但虛拟機(準确地說是編譯器)在重載時是通過參數的靜态類型而不是實際類型作為判定依據的。并且靜态類型是編譯期可知的,是以,在編譯階段,Javac 編譯器會根據參數的靜态類型決定使用哪個重載版本,是以選擇了 sayHello(Human) 作為調用目标。所有依賴靜态類型來定位方法執行版本的分派動作稱為靜态分派。 靜态分派的典型應用是方法重載。
  • 靜态分派發生在編譯階段,是以确定靜态分派的動作實際上不是由虛拟機來執行的。 是以代碼運作結果如下:
    JVM 7:方法的調用
    JVM 7:方法的調用
  • 總結:方法會根據你送入的參數有不同的表現形式,這個就是分派。方法隻認識傳入參數的最原始的外觀類型。
3.1.2動态分派
  • 多見于方法的重寫。(重寫:在子類中将父類的成員方法的名稱保留,重新編寫成員方法的實作内容,更改方法的通路權限,修改返 回類型的為父類傳回類型的子類。)
  • 另外一個例子:
    JVM 7:方法的調用
  • 重寫也是使用 invokevirtual 指令,隻是這個時候具備多态性。
  • invokevirtual 指令有多态查找的機制,該指令運作時,解析過程如下:
  1. 找到操作數棧頂的第一個元素所指向的對象實際類型,記做 c;
  2. 如果在類型 c 中找到與常量中的描述符和簡單名稱都相符的方法,則進行通路權限校驗,如果通過則傳回這個方法直接引用,查找過程結束,不通過則傳回 java.lang.IllegalAccessError;
  3. 否則,按照繼承關系從下往上依次對 c 的各個父類進行第二步的搜尋和驗證過程;
  4. 如果始終沒找到合适的方法,則抛出 java.lang.AbstractMethodError 異常,這就是 Java 語言中方法重寫的本質。
  • 對應虛拟機棧中棧中的内容,引出動态連結的概念:
  • invokevirtual 可以知道方法 call()的符号引用轉換是在運作時期完成的,在方法調用的時候。部分符号引用在運作期間轉化為直接引用,這種轉化就是動态連結。

方法表:

  • 動态分派會執行非常頻繁的動作,JVM 運作時會頻繁的、反複的去搜尋中繼資料,是以 JVM 使用了一種優化手段,這個就是在方法區中建立一個虛方法表。 使用虛方法表索引來替代中繼資料查找以提高性能。
  • 在實作上,最常用的手段就是為類在方法區中建立一個虛方法表。虛方法表中存放着各個方法的實際入口位址。如果某個方法在子類中沒有被重寫,那 子類的虛方法表裡面的位址入口和父類相同方法的位址入口是一緻的,都指向父類的實作入口。如果子類中重寫了這個方法,子類方法表中的位址将會 替換為指向子類實作版本的入口位址。上圖中,Son 重寫了來自 Father 的全部方法,是以 Son 的方法表沒有指向 Father 類型資料的箭頭。但是 Son 和 Father 都沒有重寫來自 Object 的方法,是以它們的方法表中所有從 Object 繼承來的方法都指向了 Object 的資料類型。
    JVM 7:方法的調用

3.2接口調用

  • invokeinterface 和 invokevirtual 指令類似,不過作用于接口類;
    JVM 7:方法的調用

3.3Lambda 表達式

  • invokedynamic 這個位元組碼是比較複雜。和反射類似,它用于一些動态的調用場景,但它和反射有着本質的不同,效率也比反射要高得多。
3.3.1invokedynamic
  • 這個指令通常在 Lambda 文法中出現,看下段代碼:
    JVM 7:方法的調用
  • 使用 javap -p -v 指令可以在 main 方法中看到 invokedynamic 指令:
    JVM 7:方法的調用
  • 看 javap 的輸出:
    JVM 7:方法的調用
  • BootstrapMethods 屬性在 Java 1.7 以後才有,位于類檔案的屬性清單中,這個屬性用于儲存 invokedynamic 指令引用的引導方法限定符。
  • 和上面介紹的四個指令不同,invokedynamic 并沒有确切的接受對象,取而代之的,是一個叫 CallSite 的對象。
3.3.2方法句柄(MethodHandle)
  • 官方文檔解釋:https://docs.oracle.com/javase/7/docs/api/java/lang/invoke/MethodHandles.html
  • invokedynamic 指令的底層,是使用方法句柄(MethodHandle)來實作的。方法句柄是一個能夠被執行的引用,它可以指向靜态方法和執行個體方法,以及虛 構的 get 和 set 方法,從以下案例中可以看到 MethodHandle 提供的一些方法。
    JVM 7:方法的調用
  • MethodHandle 是什麼?簡單的說就是方法句柄,通過這個句柄可以調用相應的方法。
  • 用 MethodHandle 調用方法的流程為:

    (1) 建立 MethodType,擷取指定方法的簽名(出參和入參)

    (2) 在 Lookup 中查找 MethodType 的方法句柄 MethodHandle

    (3) 傳入方法參數通過 MethodHandle 調用方法

代碼示例

JVM 7:方法的調用
JVM 7:方法的調用
JVM 7:方法的調用

MethodType

  • MethodType 表示一個方法類型的對象,每個 MethodHandle 都有一個 MethodType 執行個體,MethodType 用來指明方法的傳回類型和參數類型。其有多個工廠方法的重載。
    JVM 7:方法的調用
    JVM 7:方法的調用

Lookup

  • MethodHandle.Lookup 可以通過相應的 findxxx 方法得到相應的 MethodHandle,相當于 MethodHandle 的工廠方法。查找對象上的工廠方法對應于方法、 構造函數和字段的所有主要用例。
  • findStatic 相當于得到的是一個 static 方法的句柄(類似于 invokestatic 的作用),findVirtual 找的是普通方法(類似于 invokevirtual 的作用)
    JVM 7:方法的調用

invoke

  • invoke 和 invokeExact,前者在調用的時候可以進行傳回值和參數的類型轉換工作,而後者是精确比對的。
    JVM 7:方法的調用
3.3.3Lambda 表達式的捕獲與非捕獲
  • 當 Lambda 表達式通路一個定義在 Lambda 表達式體外的非靜态變量或者對象時,這個 Lambda 表達式稱為“捕獲的”
    JVM 7:方法的調用
  • 那麼“非捕獲”的 Lambda 表達式來就是 Lambda 表達式沒有通路一個定義在 Lambda 表達式體外的非靜态變量或者對象
    JVM 7:方法的調用
  • Lambda 表達式是否是捕獲的和性能悄然相關。一個非捕獲的 lambda 通常比捕獲的更高效,非捕獲的 lambda 隻需要計算一次. 然後每次使用到它都會返 回一個唯一的執行個體。而捕獲的 lambda 表達式每次使用時都需要重新計算一次,而且從目前實作來看,它很像執行個體化一個匿名内部類的執行個體。
  • lambda 最差的情況性能内部類一樣, 好的情況肯定比内部類性能高。
  • Oracle 公司的性能比較的文檔,詳細而全面的比較了 lambda 表達式和匿名函數之間的性能差别。
  • lambda 開發組也有一篇 PPT 其中也講到了 lambda 的性能(包括 capture 和非 capture 的情況)。 lambda 最差的情況性能内部類一樣, 好的情況肯定比内部類性能高。

    https://www.oracle.com/technetwork/java/jvmls2013kuksen-2014088.pdf

    http://nerds-central.blogspot.tw/2013/03/java-8-lambdas-they-are-fast-very-fast.html

  • Lambda 語言實際上是通過方法句柄來完成的,大緻這麼實作(JVM 編譯的時候使用 invokedynamic 實作 Lambda 表達式,invokedynamic 的是使用 MethodHandle 實作的,是以 JVM 會根據你編寫的 Lambda 表達式的代碼,編譯出一套可以去調用 MethodHandle 的位元組碼代碼,參考執行個體類:MethodHandleDemo)
  • 句柄類型(MethodType)是我們對方法的具體描述,配合方法名稱,能夠定位到一類函數。通路方法句柄和調用原來的指令基本一緻,但它的調用異常, 包括一些權限檢查,在運作時才能被發現。
  • 案例中,完成了動态語言的特性,通過方法名稱和傳入的對象主體,進行不同的調用,而 Bike 和 Man 類,可以沒有任何關系。
  • 可以看到 Lambda 語言實際上是通過方法句柄來完成的,在調用鍊上自然也多了一些調用步驟,那麼在性能上,是否就意味着 Lambda 性能低呢?對于大部分“非捕獲”的 Lambda 表達式來說,JIT 編譯器的逃逸分析能夠優化這部分差異,性能和傳統方式無異;但對于“捕獲型”的表達式來說,則需要通過方法句柄,不斷地生成擴充卡,性能自然就低了很多(不過和便捷性相比,一丁點性能損失是可接受的)。
  • invokedynamic 指令,它實際上是通過方法句柄來實作的。使用 Lambda 表達式時,盡量使用“非捕獲”的。
jvm