Java:方法的虛分派(virtual dispatch)和方法表(method table)
本文通過介紹 Java 方法呼叫的虛分派,來加深對 Java 多型實現的理解。需要預先理解 Java 位元組碼和 JVM 的基本框架。
虛分配(Virtual Dispatch)
首先從位元組碼中對方法的呼叫說起。Java 的 bytecode 中方法的呼叫實現分為四種指令:
- 1.invokevirtual 為最常見的情況,包含 virtual dispatch 機制;
- 2.invokespecial 是作為對 private 和構造方法的呼叫,繞過了 virtual dispatch;
- 3.invokeinterface 的實現跟 invokevirtual 類似。
- 4.invokestatic 是對靜態方法的呼叫。
其中最複雜的要屬 invokevirtual 指令,它涉及到了多型的特性,使用 virtual dispatch 做方法呼叫。
virtual dispatch 機制會首先從 receiver(被呼叫方法的物件)的類的實現中查詢對應的方法,如果沒找到,則去父類查詢,直到找到函式並實現呼叫,而不是依賴於引用的型別。
下面是一段有趣的程式碼。反映了 virtual dispatch 機制 和 一般的 field 訪問的不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
執行的結果為
1 2 3 |
|
前兩行輸出中,對於 intro 這個屬性的訪問,直接指向了父類中的變數,因為引用型別為父類。
第二行對於 target()的方法呼叫,則是指向了子類中的方法,雖然引用型別也為父類,但這是虛分派的結果,虛分派不管引用型別的,只查被呼叫物件的型別。
既然虛分派機制是從被呼叫物件本身的類開始查詢,那麼對於一個覆蓋了父類中某方法的子類的物件,是無法呼叫父類中那個被覆蓋的方法的嗎?
在虛分派機制中這確實是不可以的。但卻可以通過 invokespecial 實現。如下程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
func()就成功的呼叫了父類的方法 target(),雖然 target()已經被子類重寫了。具體的呼叫細節,從位元組碼中可以看到:
1 2 3 |
|
其中使用了 invokespecial 指令,而不是施行虛分派策略的 invokevirtual 指令。
方法表(Method Table)
介紹了虛分派,接下來介紹是它的一種實現方式 – 方法表。類似於 C++的虛擬函式表 vtbl。
在有的 JVM 實現中,使用了方法表機制實現虛分派,而有時候,為了節省記憶體可能不採用方法表的實現。
不要被方法表這個名字迷惑,它並不是記錄所有方法的表。它是為虛分派服務,不會記錄用 invokestatic 呼叫的靜態方法和用 invokespecial 呼叫的建構函式和私有方法。
JVM 會在連結類的過程中,給類分配相應的方法表記憶體空間。每個類對應一個方法表。這些都是存在於 method area 區中的。這裡與 C++略有不同,C++中每個物件的第一個指標就是指向了相應的虛擬函式表。而 Java 中每個物件索引到對應的類,在對應的類資料中對應一個方法表。(關於連結的更多資訊,參見博文《Java 類的裝載、連結和初始化》)
一種方法表的實現如下:
父類的方法比子類的方法先得到解析,即父類的方法相比子類的方法位於表的前列。
表中每項對應於一個方法,索引到實際方法的實現程式碼上。如果子類重寫了父類中某個方法的程式碼,則該方法第一次出現的位置的索引更換到子類的實現程式碼上,而不會在方法表中出現新的項。
JVM 執行時,當代碼索引到一個方法時,是根據它在方法表中的偏移量來實現訪問的。(第一次執行到呼叫指令時,會執行解析,將符號索引替換為對應的直接索引)。
由於 invokevirtual 呼叫的方法在對應的類的方法表中都有固定的位置,直接索引的值可以用偏移量來表示。(符號索引解析的最終目的是完成直接索引:物件方法和物件變數的呼叫都是用偏移量來表示直接索引的)
invokeinterface 與 invokevirtual 的比較
當使用 invokeinterface 來呼叫方法時,由於不同的類可以實現同一 interface,我們無法確定在某個類中的 inteface 中的方法處在哪個位置。於是,也就無法解析 CONSTANT_intefaceMethodref-info 為直接索引,而必須每次都執行一次在 methodtable 中的搜尋了。 所以,在這種實現中,通過 invokeinterface 訪問方法比通過 invokevirtual 訪問明顯慢很多。