JVM方法調用


JVM的靜態綁定和動態綁定

JVM識別方法的關鍵在於類名、方法名及方法描述符(method descriptor)。方法描述符是由方法的參數類型和返回類型所構成。在同一個類中,如果同時出現多個類名方法名以及描述符都相同的方法,java虛擬機會在類的驗證階段報錯。

java虛擬機與java語言不同,JVM不限制方法名和參數類型相同,返回類型不同的方法出現在同一個類中,對於調用這些方法的字節碼來說,由於字節碼所附帶的方法描述符包含了返回類型,因此java虛擬機能夠准確的識別目標方法。

java虛擬機的靜態綁定:指的是在解析時便能夠直接識別目標方法的情況。即在編譯時期解析,指令指向的方法就是靜態方法,也就是private、final、static和構造方法。

java虛擬機的動態綁定:指的是需要在運行過程中根據調用者的動態類型來識別目標方法的情況,比如接口和虛方法調用無法找到真正需要調用的方法,因為它可能是定義在子類中的方法,所以這種在運行時期再能明確類型的方法我們成為動態綁定。

JVM提供了如下方法調用指令:

1、invokestatic: 調用靜態方法。

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

3、invokevirtual: 調用虛方法即非私有的實例方法。

4、invokeinterface: 調用接口方法,在運行時再確定一個實現此接口的對象。

5、invokedynamic: 調用動態方法,在運行時動態解析出調用點限定符所引用的方法之后,調用該方法。

調用指令的符號引用

在編譯過程中,我們並不知道目標方法的具體內存地址。因此,java編譯器會暫時用符號應用來表示該目標方法。這一符號引用包括目標方法所在的類或接口的名字,以及目標方法的方法名或方法描述符。

符號應用存儲在class文件的常量池中。根據目標方法是否為接口。這些引用可分為接口符號引用和非接口符號引用。在執行符號引用的字節碼前,java虛擬機需要解析這些符號引用,並替換為實際引用。

虛方法的調用

java虛擬機中的虛方法調用:

1、java里所有非私有的實例方法調用編譯成invokevirtual指令

2、接口方法調用被編譯成invokeinterface指令

在絕大多數情況下,JVM需要根據調用者的動態類型,來確定虛方法調用的目標方法。這個過程我們稱之為動態綁定。性對於靜態綁定的非虛方法調用來說,虛方法調用更加耗時。

在JVM中采用空間換時間的策略來實現動態綁定,他為每個類生成一張方法表,用以快速定位目標方法。

方法表

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

這個數據結構是JVM實現動態綁定的關鍵所在。

方法表本質上是一個數組,每個數組元素指向一個當前類及其祖先類中非私有的實例方法。這些方法可能是具體的可執行的方法,也可能是沒有響應字節碼的抽象方法。

方法表滿足兩個特質:

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

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

方法調用指令中的符號引用會在執行之前解析成實際引用。對於靜態綁定的方法調用而言,實際引用指向具體的目標方法。對於動態綁定的方法而言,實際引用則是方法表中的索引值(實際並不僅是索引值)。

在執行過程中,JVM將獲取調用者的實際類型,並在該實際類型的虛方法表中,根據索引值獲取目標方法。這個過程便是動態綁定。

 

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

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

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

內聯緩存(inlining cache)

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

 多態優化相關的的術語:

1、單態(monomorphic):指的是僅有一種狀態的情況。

2、多態(polymorphic):指的是有限數量種狀態的情況,二態是多態的一種。

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

所以,內聯緩存也有對應的單態內聯緩存、多態內聯緩存、超多態內聯緩存。

單態內聯緩存:即只緩存了一種動態類型及所對應的目標方法。他的實現比較簡單,即比較所緩存的動態類型,如果命令則直接調用對應的目標方法。

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

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

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

1、替換單態內聯緩存中的記錄:

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

2、劣化為超多態狀態

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

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

invokedynamic

 該指令的調用機制抽象出調用點這一個概念,並允許應用程序將調用點鏈接至任意符合條件的方法上。

作為invokedynamic的准備工作,java7引入了更加底層、更加靈活地抽象方法:方法句柄。

方法句柄:是一個強類型的,能夠被直接執行的引用。該引用可以指向常規的靜態方法或者實例方法,也可以指向構造器或者字段。當指向字段時,方法句柄實則指向包含字段訪問字節碼的虛構方法,語義上等價於目標字段的getter/setter方法。但是它並不會直接指向目標字段所在類中的getter/setter,畢竟你無法保證已有的getter/setter方法就是在訪問目標字段。

方法句柄的類型是由所指向方法的參數類型及返回類型組成的。它是用來確認方法句柄是否適配的唯一關鍵。當時用方法句柄時其實我們並不關心方法句柄所指向方法的方法名或者類名。

方法句柄的創建是通過MethodHandles.Lookup類來完成的。它提供了多個API,既可以使用反射API中的Method來查找,也可以根據類、方法名以及方法句柄的類型來查找。

 

 

 

 

注:此文為極客時間鄭雨迪專欄,java虛擬機講解及自己查資料的學習總結。鄭雨迪《深入拆解Java虛擬機》很不錯。

 


免責聲明!

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



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