淺談 JVM 5:方法調(diào)用機制

經(jīng)歷了前文中類加載機制、JVM 邏輯區(qū)域劃分和運行時棧幀及方法字節(jié)碼執(zhí)行過程等內(nèi)容的沉淀之后,我們這次聊聊 JVM 中方法調(diào)用是如何發(fā)生的,以及靜態(tài)綁定和動態(tài)綁定是什么。
先從一段 Java 代碼及其對應(yīng)字節(jié)碼看起。
// javaprivate static int methodA() {return 100;}private void methodB() {methodA();}// methodB's bytecode0 invokestatic #73 pop4 return
Java 代碼部分比較簡單,在方法?methodB?中調(diào)用了?methodA。在?methodB?字節(jié)碼中,invokestatic #7?部分對應(yīng)了方法調(diào)用功能。我們可以發(fā)現(xiàn),代表了目標方法的?#7?符號包含類名?InvokeDemo、方法名?methodA?以及方法描述符?()I,即方法參數(shù)類型和返回值類型。
由此我們知道,invokestatic?方法調(diào)用指令需要類名、方法名、方法描述符(方法參數(shù)類型 + 返回值類型)這些信息來識別一個方法。其實除了此指令外,其他方法調(diào)用指令也都需要這些信息,無論是重載的方法還是重寫的方法。對于重載的方法,JVM 在編譯期就可以完成方法符號的解析和識別,所以對于 JVM 來說不存在重載的概念。而對于重寫方法的符號解析需要在運行期確定。
類似于重載方法這種在編譯期就可以解析的方法,可以稱之為靜態(tài)綁定;對于重寫這種在運行時才能解析的方法,可以稱之為動態(tài)綁定。
方法調(diào)用指令
除了?invokestatic?之外,方法調(diào)用還有其他幾種指令。同樣,我們先來看一段示例代碼。
public class InvokeDemo {private static int methodA() {return 100;}private void methodB() {methodA(); // invokestatic}private void methodC() {methodB(); // invokevirtual}interface IMethod {void methodD();}class MethodImpl implements IMethod {public void methodD() {}}private void methodE() {IMethod iMethod = new MethodImpl(); // invokespecialiMethod.methodD(); // invokeinterface}private void methodF() {new Thread(() -> {} // invokedynamic);}}
從上述代碼可以看出,方法調(diào)用指令包括:
?invokestatic:靜態(tài)方法的調(diào)用;?invokespecial:構(gòu)造器、私有實例等方法的調(diào)用;?invokevirtual:非私有實例方法的調(diào)用;?invokeinterface:接口方法的調(diào)用。?invokedynamic:lambda 表達式等調(diào)用。
按照綁定類型分類如下:
?靜態(tài)綁定:invokestatic、invokespecial?動態(tài)綁定:invokevirtual、invokeinterface
其中?invokedynamic?指令較為特殊,在此先不談及。
對于?invokevirtual?和?invokeinterface,大部分情況為動態(tài)綁定。但如果方法能夠唯一確定,比如被?final?修飾,就可以使用靜態(tài)綁定直接解析。
方法調(diào)用機制
方法符號解析
根據(jù)上文我們知道,對一個方法的定位需要類名、方法名、方法描述符(方法參數(shù)類型 + 返回值類型)信息。以一個類方法的方法定位為例,其過程如下:
1.在調(diào)用類中,根據(jù)方法名和描述符查找方法;2.未找到時,遞歸查找它的父類;3.仍未找到時,在其接口中查找。
經(jīng)過以上過程,方法的符號引用被解析為實際引用。對于靜態(tài)綁定,實際引用為指向方法的的指針;對于動態(tài)綁定,實際引用為方法表的索引。
方法定位與方法表
我們以?invokevirtual?和?invokeinterface?兩個指令為例,談?wù)剟討B(tài)綁定方法的調(diào)用。
JVM 采用使用空間換取時間的方式來實現(xiàn)動態(tài)綁定,為每個類生成對應(yīng)方法表來定位目標方法。方法表根據(jù)指令分為?invokevirtual?的虛方法表 vtable 和?invokeinterface?的接口方法表 itable。其中的每個元素指向當前類或祖先類的方法。
方法表的特征要求如下:
1.跟 Java 的繼承特征一致,子類方法表包含父類方法表的所有方法;2.子類方法的索引和父類對應(yīng)方法的索引一致。
方法調(diào)用
那么,動態(tài)綁定方法的調(diào)用過程如下:
1.Java 棧中調(diào)用對應(yīng)指令,如?invokestatic #7 ;2.找到方法的調(diào)用者——Java 堆中的類;3.訪問該類對應(yīng)的方法表;4.找到方法的地址,調(diào)用方法。
除此之外,JVM 還使用了內(nèi)聯(lián)緩存、方法內(nèi)聯(lián)的技術(shù)來優(yōu)化動態(tài)綁定方法的調(diào)用。內(nèi)聯(lián)緩存就是將動態(tài)綁定方法解析后的類型和目標方法緩存起來,下一次碰到相同類型直接使用。仍然是空間換時間的策略。
總結(jié)
至此,我們可以在腦中構(gòu)建出 JVM 運行字節(jié)碼的大致流程。從外部的字節(jié)碼結(jié)構(gòu)和解讀方法,到虛擬機加載字節(jié)碼流的過程,然后是虛擬機邏輯區(qū)域劃分和每部分的功能,以及 Java 棧中的計算過程,最后是具體方法調(diào)用指令的解析、調(diào)用機制。你可以查看以下文章進行回顧。
當然,JVM 相關(guān)的內(nèi)容不止這些。比如 Java 堆的內(nèi)存管理和回收機制、JVM 的啟動過程、JNI 調(diào)用機制等,我們還未涉及。
另外,當初使用 Java 作為官方語言的 Android,其內(nèi)部在前期使用的 Dalvik 虛擬機和后來的 ART 虛擬機在結(jié)構(gòu)和運行字節(jié)碼上又有何不同?當下力推的 Kotlin 在虛擬機的執(zhí)行上和 Java 有什么不同嗎?
我們有時間再聊。
推薦
工具:leetcode 的 VSCode、IDEA 插件。推薦原因:可以方便的瀏覽、提交算法題,和諧地在 IDE 中刷題。
