JVM方法調用過程


  JVM方法調用過程

  重載和重寫

  同一個類中,如果出現多個名稱相同,並且參數類型相同的方法,將無法通過編譯.因此,想要在同一個類中定義名字相同的方法,那么它們的參數類型必須不同.這種方法上的聯系就是重載.

  重載的方法在編譯過程中即可完成識別.具體到每一個方法調用,Java編譯器會根據所傳入參數的聲明類型(有別實際類型)來選取重載方法.

  選取過程如下:

    1.不考慮對基本類型自動裝拆箱(auto-boxing,auto-unboxing),以及可變長參數的情況下選取重載方法;

    2.如果1中未找到適配的方法,則允許自動裝拆箱,但不允許可變長參數的情況下選取重載方法;

    3.如果2中未找到適配的方法,則在允許自動裝拆箱以及可變長參數的情況下選取重載方法.

  JVM的靜態綁定和動態綁定

  Java虛擬機識別方法的關鍵在於類名/方法名/方法描述符(method descriptor).注:方法描述符由方法的參數類型/返回類型構成.

  Java虛擬機中的靜態綁定(static binding)指的是在解析時便能夠直接識別目標方法的情況;而動態綁定(dynamic binding)則指的是需要在運行過程中根據調用者的動態類型來識別目標方法的情況.

  具體來說,Java字節碼中與調用相關的指令共有五種:

    1.invokestatic:用於調用靜態方法

    2.invokespecial:用於調用私有實例方法/構造器,以及使用super關鍵字調用父類的實例方法/構造器,和所有實現接口的默認方法

    3.invokevirtual:用於調用非私有實例方法

    4.invokeinterface:用於調用接口方法

    5.invokedynamic:用於調用動態方法

  示例代碼如下:

interface 客戶 {
  boolean isVIP();
}

class 商戶 {
  public double 折后價格 (double 原價, 客戶 某客戶) {
    return 原價 * 0.8d;
  }
}

class 奸商 extends 商戶 {
  @Override
  public double 折后價格 (double 原價, 客戶 某客戶) {
    if (某客戶.isVIP()) {                         // invokeinterface      
      return 原價 * 價格歧視 ();                    // invokestatic
    } else {
      return super. 折后價格 (原價, 某客戶);          // invokespecial
    }
  }
  public static double 價格歧視 () {
    // 咱們的殺熟算法太粗暴了,應該將客戶城市作為隨機數生成器的種子。
    return new Random()                          // invokespecial
           .nextDouble()                         // invokevirtual
           + 0.8d;
  }
}

  調用指令的符號引用

  在編譯過程中,目標方法的具體內存地址尚未確定.這時,Java編譯器會暫時用符號引用來表示該目標方法.這一符號引用包括目標方法所在的類或接口的名字,以及目標方法的方法名和方法描述符.

  符號引用存儲在class文件的常量池中.根據目標方法是否為接口方法,又可分為接口符號引用和非接口符號引用.

  對於非接口符號引用,假定該符號引用所指向的類為 C,則 Java 虛擬機會按照如下步驟進行查找。

    1.在 C 中查找符合名字及描述符的方法。

    2.如果沒有找到,在 C 的父類中繼續搜索,直至 Object 類。

    3.如果沒有找到,在 C 所直接實現或間接實現的接口中搜索,這一步搜索得到的目標方法必須是非私有、非靜態的。並且,如果目標方法在間接實現的接口中,則需滿足 C 與該接口之間沒有其他符合條件的目標方法。如果有多個符合條件的目標方法,則任意返回其中一個。

  從這個解析算法可以看出,靜態方法也可以通過子類來調用。此外,子類的靜態方法會隱藏(注意與重寫區分)父類中的同名、同描述符的靜態方法。

  對於接口符號引用,假定該符號引用所指向的接口為 I,則 Java 虛擬機會按照如下步驟進行查找。

    1.在 I 中查找符合名字及描述符的方法。

    2.如果沒有找到,在 Object 類中的公有實例方法中搜索。

    3.如果沒有找到,則在 I 的超接口中搜索。這一步的搜索結果的要求與非接口符號引用步驟 3 的要求一致。

  經過上述的解析步驟之后,符號引用會被解析成實際引用。對於可以靜態綁定的方法調用而言,實際引用是一個指向方法的指針。對於需要動態綁定的方法調用而言,實際引用則是一個方法表的索引。

  虛方法調用

  所有非私有實例方法被調用-->編譯-->invokevirtual指令.

  接口方法調用-->編譯-->invokeinterface指令. 

  這兩種指令,均屬於Java虛擬機中的虛方法調用.

  多數情況下,Java虛擬機需要根據調用者的動態類型-->確定虛方法調用的目標方法.這個過程被稱為動態綁定.相對於靜態綁定的非虛方法調用,虛方法調用更加耗時.

  在Java虛擬機中,靜態綁定包括用於調用靜態方法的invokestatic指令,和用於調用構造器/私有實例方法/超類非私有實例方法的invokespecial指令.

  如果虛方法調用指向一個標記為final的方法,那么Java虛擬機也可以靜態綁定該虛方法調用的目標方法.

  Java虛擬機采用了一種用空間換時間的策略來實現動態綁定.它為每個類生成一張方法表,用以快速定位目標方法.

  方法表

  類加載的准備階段,除了為靜態字段分配內存外,還會構建與該類相關聯的方法表.

  方法表,時Java虛擬機實現動態綁定的關鍵所在.

  方法表本質上是一個數組,每個數組元素指向一個當前類及其父類中非私有的實例方法.

  方法表滿足兩個特質:

    1.子類方法表中包含父類方法表中的所有方法

    2.子類方法在方法表中的索引值,與它所重寫的父類方法的索引值相同.

  pre:方法調用指令中的符號引用會在執行之前解析為實際引用.

    靜態綁定的方法調用:實際引用-->具體的目標方法

    動態綁定的方法調用:實際引用-->方法表的索引值(實際上不止索引值)

  在執行過程中,Java虛擬機將獲取調用者的實際類型,並在該實際類型的虛方法表中,根據索引值獲得目標方法--->動態綁定的過程

  in fact,使用了方法表的動態綁定與靜態綁定相比,僅僅多出幾個內存解引用操作 : 訪問棧上的調用者,讀取調用者的動態類型,讀取該類型的方法表,讀取方法表中某個索引值所對應的目標方法.相對於創建並初始化Java棧幀來說,這幾個內存解引用操作的開銷可以忽略不計.

  但是,虛方法調用對性能仍有影響:

    方法表的引入帶來的優化效果僅存在與解釋執行或者即時編譯代碼的最壞情況下.而且即時編譯還擁有兩個性能更好的優化手段:內聯緩存(inlining cache)和方法內聯(method inlining).

  內聯緩存

  內聯緩存是一種加快動態綁定的優化技術.它能夠緩存虛方法調用中調用者的動態類型,以及該類型所對應的目標方法.后續執行中,優先使用緩存,沒有則使用基於方法表的動態綁定.

  對多態的優化,術語:

    1.單態(monomorphic),指的是僅有一種狀態的情況

    2.多態(polymorphic),指的是有限數量種狀態的情況.二態(bimorphic)是多態的其中一種.

    3.超多態(megamorphic),指的是更多種狀態的情況.通常用某個閾值來區分多態和超多態.

  綜上,內聯緩存對應單態內聯緩存/多態內聯緩存/超多態內聯緩存.

  1.單態內聯緩存,顧名思義,便是只緩存了一種動態類型以及它所對應的目標方法。它的實現非常簡單:比較所緩存的動態類型,如果命中,則直接調用對應的目標方法。

  2.多態內聯緩存則緩存了多個動態類型及其目標方法。它需要逐個將所緩存的動態類型與當前動態類型進行比較,如果命中,則調用對應的目標方法。

    注:一般來說,我們會將更加熱門的動態類型放在前面。在實踐中,大部分的虛方法調用均是單態的,也就是只有一種動態類型。為了節省內存空間,Java 虛擬機只采用單態內聯緩存。

  在選擇內聯緩存時,如果未命中則重新使用方法表做動態綁定.這時有兩種選擇:

    1.替換單態內聯緩存中的紀錄。這種做法就好比 CPU 中的數據緩存,它對數據的局部性有要求,即在替換內聯緩存之后的一段時間內,方法調用的調用者的動態類型應當保持一致,從而能夠有效地利用內聯緩存。因此,在最壞情況下,用兩種不同類型的調用者,輪流執行該方法調用,那么每次進行方法調用都將替換內聯緩存。也就是說,只有寫緩存的額外開銷,而沒有用緩存的性能提升。

    2.劣化為超多態狀態。這也是 Java 虛擬機的具體實現方式。處於這種狀態下的內聯緩存,實際上放棄了優化的機會。它將直接訪問方法表,來動態綁定目標方法。與替換內聯緩存紀錄的做法相比,它犧牲了優化的機會,但是節省了寫緩存的額外開銷。

  雖然內聯緩存附帶內聯二字,但是它並沒有內聯目標方法。這里需要明確的是,任何方法調用除非被內聯,否則都會有固定開銷。這些開銷來源於保存程序在該方法中的執行位置,以及新建、壓入和彈出新方法所使用的棧幀。

  JVM處理invokedynamic

  在Java中,方法調用會編譯為invokestatic/invokespecial/invokevirtual/invokeinterface四種指令.這些類名與包含目標方法類名/方法名/方法描述符的符號引用捆綁.在實際運行之前,Java虛擬機將根據這個符號引用鏈接到具體的目標方法.

  Java7引入了invokedynamic指令,該指令的調用機制抽象出調用點這一概念,並允許應用程序將調用點鏈接至任何符合條件的方法上.

  作為invokedynamic的准備工作,Java7引入了更加底層/更加靈活的方法抽象:方法句柄(MethodHandle).

  方法句柄的概念

  方法句柄是一種強類型的,能夠被直接執行的引用.該引用可以指向常規的靜態方法或者實例方法,也可以指向構造器或者字段.當指向字段時,方法句柄實則指向包含字段訪問字節碼的虛構方法,語義上等價於目標字段的getter或者setter方法.

  HotSpot虛擬機中方法句柄調用的具體實現 :

    以DirectMethodHandle為例,調用方法句柄所使用的invokeExact或者invoke方法具備簽名多態性的特性.會根據具體的傳入參數來生成方法描述符.其中,invokeExact要求傳入的參數和所指向方法的描述符嚴格匹配.方法句柄還支持增刪改參數的操作,這些操作時通過生成另一個充當適配器的方法句柄來實現的.

    方法句柄的調用和反射調用一樣,都是間接調用.同樣都面臨無法內聯的問題,不過與反射調用不同的是,方法句柄的內聯瓶頸在於即時編譯器能否將該方法句柄識別為常量.

  invokedynamic指令

  invokedynamic是Java7引入的一條新指令,用以支持動態語言的方法調用.具體來說,它將調用點(CallSite)抽象成一個Java類,並且將原本由Java虛擬機控制的方法調用以及方法鏈接暴露給了應用程序.在運行過程中,每一條invokedynamic指令將捆綁一個調用點,並會調用該調用點所鏈接的方法句柄.

  在第一次執行invokedynamic指令時,Java虛擬機會調用該指令所對應的啟動方法(BootStrapMethod),來生成調用點,並將之綁定至該invokedynamic指令中.在之后的運行過程中,Java虛擬機則會直接調用綁定的調用點所鏈接的方法句柄.

  在字節碼中,啟動方法是用方法句柄來指定的.這個方法句柄指向一個返回類型為調用點的靜態方法.該方法必須接收三個固定的參數,分別為一個Lookup類實例,一個用來指代目標方法名字的字符串,以及該調用點能夠鏈接的方法句柄的類型.

  除了三個必須參數外,啟動方法(BootStrapMethod)還可以接收若干個其它的參數,用來輔助生成調用點,或者定位索要鏈接的目標方法.

  Java8的Lambda表達式

  在Java8中,Lambda表達式也是借助invokedynamic來實現的

  具體來說,Java編譯器利用invokedynamic指令來生成實現了函數式接口的適配器.這里的函數式接口指的是僅包括一個非default接口方法的接口,一般通過@FunctionalInterface注解.同時,該invokedynamic指令對應的啟動方法將通過ASM生成一個適配器類.

  對於沒有捕獲其它變量的Lambda表達式,該invokedynamic指令始終返回同一個適配器類的實例.對於捕獲了其它變量的Lambda表達式,每次執行invokedynamic指令將新建一個適配器類實例.

  不管是捕獲型的還是未捕獲型的Lambda表達式,它們的性能上限皆可以達到直接調用的性能.其中,捕獲型Lambda表達式借助了即時編譯器的逃逸分析,來避免實際的新建適配器類實例的操作.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM