1. 程式人生 > >Java:方法的虛分派(virtual dispatch)和方法表(method table)

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
public class Greeting
{
String intro = "Hello"; String target(){ return "world"; } } public class FrenchGreeting extends Greeting { String intro = "Bonjour"; String target(){ return "le monde"; } public static void main(String[] args){ Greeting english = new Greeting();
Greeting french = new FrenchGreeting(); System.out.println(english.intro + "," + english.target()); System.out.println(french.intro + "," + french.target()); System.out.println(((FrenchGreeting)french).intro + "," + ((FrenchGreeting)french).target()); } }

執行的結果為

1
2
3
Hello,world
Hello,le monde
Bonjour,le monde

前兩行輸出中,對於 intro 這個屬性的訪問,直接指向了父類中的變數,因為引用型別為父類。

第二行對於 target()的方法呼叫,則是指向了子類中的方法,雖然引用型別也為父類,但這是虛分派的結果,虛分派不管引用型別的,只查被呼叫物件的型別。

既然虛分派機制是從被呼叫物件本身的類開始查詢,那麼對於一個覆蓋了父類中某方法的子類的物件,是無法呼叫父類中那個被覆蓋的方法的嗎?

在虛分派機制中這確實是不可以的。但卻可以通過 invokespecial 實現。如下程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FrenchGreeting extends Greeting {
    String intro = "Bonjour";
    String target(){
        return "le monde";
    }
    public String func(){
        return super.target();
    }
    public static void main(String[] args){
        Greeting english = new Greeting();
        FrenchGreeting french = new FrenchGreeting();
        System.out.println(french.func());
    }
}

func()就成功的呼叫了父類的方法 target(),雖然 target()已經被子類重寫了。具體的呼叫細節,從位元組碼中可以看到:

1
2
3
ALOAD 0: this
INVOKESPECIAL Greeting.target() : String
ARETURN

其中使用了 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 訪問明顯慢很多。

參考資料