《深入理解java虛擬機》讀書筆記——方法的解析調用與分派調用

方法調用並不等同於方法執行,方法調用階段唯一的任務就是確定被調用方法的版本(即調用哪一個方法),暫時還不涉及方法內部的具體運行過程。Class文件的編譯過程中不包含傳統編譯中的連接步驟,一切方法調用在Class文件裏面存儲的都只是符號引用,而不是方法在實際運行時內存佈局中的入口地址(即直接引用)。這個特性需要在類加載期間,甚至到運行期間才能確定目標方法的直接引用。


一、解析調用

在類加載的解析階段,會將Class文件裏面的一部分符號引用轉化爲直接引用,這種解析能成立的前提是:方法在程序運行之前就有一個可確定的調用版本,並且這個調用版本在運行期是不可改變的。即調用目標在程序代碼寫好、編譯器進行編譯時就必須確定下來,這種方法的調用就稱爲解析。

符合“編譯期可知,運行期不可變”這個要求的方法,主要包括靜態方法私有方法兩大類。前者與類型直接關聯,後者在外部不可被訪問,這兩種方法各自的特點決定了它們都不可能通過繼承或別的方式重寫其他版本。

與之相對應的,在java虛擬中提供了5條方法調用字節碼指令:


只要能被invokestatic和invokespecial指令調用的方法,都可以在解析階段中確定唯一的版本,符合這個條件的有靜態方法私有方法實例構造器父類方法4類。它們在類加載的時候就會把符號引用解析爲該方法的直接引用,這些方法稱爲非虛方法,由於final方法不能被覆蓋,也屬於非虛方法,其他方法就稱爲虛方法


二、分派調用

1、靜態分派(發生在編譯時期)

所有依賴靜態類型來定位方法執行版本的分派動作稱爲靜態分派。靜態分派的典型應用是方法重載

下面看這段代碼:





可能你會問,爲什麼會選擇參數類型爲Human的重載呢。在解決這個問題之前,需要明確兩個概念:

Human man = new Man();

上面這行代碼中的 "Human"稱爲變量的靜態類型(Static Type),或者叫做外觀類型(Apparent Type),後面的“Man”則稱爲變量的實際類型(Actual Type)。

靜態類型和實際類型的區別在於:靜態類型的變化僅僅是在使用時發生變化,變量本身的靜態類型不會發生改變,並且最終的靜態類型是在編譯期可知;而實際類型變化的結果在運行期纔可確定。例如下面代碼:


虛擬機在重載時是通過參數的靜態類型而不是實際類型作爲判定依據的。並且靜態類型是編譯期可知的,因此,在編譯期,Javac 編譯器會根據參數的靜態類型決定使用哪個重載版本,所以選擇了 sayHello(Human),並把這個方法的符號引用寫入 main() 方法的兩條 invokevirtual 指令的參數中。


2、動態分派(發生在運行時期)

運行期根據實際類型確定方法執行版本的分派過程稱爲動態分派。它和重寫(Override)有着很密切的關聯。

還是sayHello的例子。






通過javap輸出這段代碼的字節碼:




16~21句是關鍵部分,16、20兩句將創建的兩個對象的引用壓到棧頂,這兩個對象是將要執行的 sayHello() 方法的所有者,稱爲接受者(Receiver);17、21句是方法調用指令,但是這兩條指令的最終執行的目標方法並不相同。原因就需要從 invokevirtual 指令的動態查找過程開始說起, invokevirtual 指令的運行時解析過程大致分爲以下幾個步驟:
1、找到操作數棧頂的第一個元素所指向的對象的實際類型,記做 C。
2、如果在類型 C 中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗。如果通過,則返回這個方法的直接引用,查找過程結束:如果不通過,則返回 java.lang.IllegalAccessError 異常。
3、否則,按照繼承關係從下往上依次對C 的各個父類進行第2步的搜索和驗證過程。
4、如果始終沒有找到合適的方法,則拋出 java.lang.AbstractMethodError 異常。

由於invokevirtual指令執行的第一步就是在運行期確定接收者的實際類型,所以兩次調用中的invokevirtual指令把常量池中的類方法符號引用解析到了不同的直接引用上,這個過程就是java語言方法重寫的本質。


三、單分派和多分派

方法的接收者與方法的參數統稱爲方法的宗量。根據分派基於多少種宗量,可以把分派劃分成單分派和多分派。單分派是根據一個宗量對目標方法進行選擇,多分派是根據多於一個宗量對目標方法進行選擇。

看下面的代碼:





靜態分派的過程的選擇目標方法的依據有兩條:

     1、靜態類型是Father還是Son

     2、方法參數是QQ還是360

選擇結果的最終產物是產生了兩條invokevirtual指令,兩條指令的參數分別爲常量池中指向Father.hardChoice(360)及Father.hardChoice(QQ)方法的符號引用。因爲是根據兩個宗量進行選擇,所以靜態分派屬於多分派類型


在動態分派的過程中,由於編譯器已經決定了目標方法的簽名,因此只需要找到方法的接受者的實際類型就可以了。因爲是根據一個宗量進行選擇,所以動態分派屬於單分派類型


四、虛擬機動態分派的實現

由於動態分派是非常頻繁的動作,而且動態分派的方法版本選擇過程需要運行時在類的方法元數據中搜索合適的目標方法。虛擬機出於對性能的考慮,最常用的“穩定優化”的手段就是爲類在方法區中建立一個虛方法表(Virtual Method Table,也成爲vtable,與此對應的,在invokeinterface執行時也用到接口方法表——Interface Method Table,簡稱itable),使用方法表索引來代替元數據查找以提高性能。

虛方法表中存放着各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那子類的虛方法表裏面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實現入口。如果子類中重寫了這個方法,子類方法表中的地址將會替換爲指向子類實現版本的入口。如上圖,Son 重寫了來自於 Father 的全部方法,因此 Son 的方法表沒有指向 Father 類型數據的箭頭。但是 Son 和 Father 都沒有重寫來自於 Object 的方法,所以他們的方法表中所有從 Object 繼承來的方法都指向了 Object 的數據類型。
方法表一般在類加載的連接階段進行初始化,準備了類的變量初始值之後,虛擬機會把該類的方法表也初始化完畢。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章