原文地址:https://blog.csdn.net/huangrunqing/article/details/51996424

.

眾所周知,多型是面向物件程式語言的重要特性,它允許基類的指標或引用指向派生類的物件,而在具體訪問時實現方法的動態繫結。C++ 和 Java 作為當前最為流行的兩種面向物件程式語言,其內部對於多型的支援到底是如何實現的呢,本文對此做了全面的介紹。

注意到在本文中,指標和引用會互換使用,它們僅是一個抽象概念,表示和另一個物件的連線關係,無須在意其具體的實現。

Java 的實現方式

Java 對於方法呼叫動態繫結的實現主要依賴於方法表,但通過類引用呼叫和介面引用呼叫的實現則有所不同。總體而言,當某個方法被呼叫時,JVM 首先要查詢相應的常量池,得到方法的符號引用,並查詢呼叫類的方法表以確定該方法的直接引用,最後才真正呼叫該方法。以下分別對該過程中涉及到的相關部分做詳細介紹。

JVM 的結構

典型的 Java 虛擬機器的執行時結構如下圖所示

圖 1.JVM 執行時結構
圖 1.JVM 執行時結構

此結構中,我們只探討和本文密切相關的方法區 (method area)。當程式執行需要某個類的定義時,載入子系統 (class loader subsystem) 裝入所需的 class 檔案,並在內部建立該類的型別資訊,這個型別資訊就存貯在方法區。型別資訊一般包括該類的方法程式碼、類變數、成員變數的定義等等。可以說,型別資訊就是類的 Java 檔案在執行時的內部結構,包含了改類的所有在 Java 檔案中定義的資訊。

注意到,該型別資訊和 class 物件是不同的。class 物件是 JVM 在載入某個類後於堆 (heap) 中建立的代表該類的物件,可以通過該 class 物件訪問到該型別資訊。比如最典型的應用,在 Java 反射中應用 class 物件訪問到該類支援的所有方法,定義的成員變數等等。可以想象,JVM 在型別資訊和 class 物件中維護著它們彼此的引用以便互相訪問。兩者的關係可以類比於程序物件與真正的程序之間的關係。

Java 的方法呼叫方式

Java 的方法呼叫有兩類,動態方法呼叫與靜態方法呼叫。靜態方法呼叫是指對於類的靜態方法的呼叫方式,是靜態繫結的;而動態方法呼叫需要有方法呼叫所作用的物件,是動態繫結的。類呼叫 (invokestatic) 是在編譯時刻就已經確定好具體呼叫方法的情況,而例項呼叫 (invokevirtual) 則是在呼叫的時候才確定具體的呼叫方法,這就是動態繫結,也是多型要解決的核心問題。

JVM 的方法呼叫指令有四個,分別是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前兩個是靜態繫結,後兩個是動態繫結的。本文也可以說是對於 JVM 後兩種呼叫實現的考察。

常量池(constant pool)

常量池中儲存的是一個 Java 類引用的一些常量資訊,包含一些字串常量及對於類的符號引用資訊等。Java 程式碼編譯生成的類檔案中的常量池是靜態常量池,當類被載入到虛擬機器內部的時候,在記憶體中產生類的常量池叫執行時常量池。

常量池在邏輯上可以分成多個表,每個表包含一類的常量資訊,本文只探討對於 Java 呼叫相關的常量池表。

CONSTANT_Utf8_info

字串常量表,該表包含該類所使用的所有字串常量,比如程式碼中的字串引用、引用的類名、方法的名字、其他引用的類與方法的字串描述等等。其餘常量池表中所涉及到的任何常量字串都被索引至該表。

CONSTANT_Class_info

類資訊表,包含任何被引用的類或介面的符號引用,每一個條目主要包含一個索引,指向 CONSTANT_Utf8_info 表,表示該類或介面的全限定名。

CONSTANT_NameAndType_info

名字型別表,包含引用的任意方法或欄位的名稱和描述符資訊在字串常量表中的索引。

CONSTANT_InterfaceMethodref_info

介面方法引用表,包含引用的任何介面方法的描述資訊,主要包括類資訊索引和名字型別索引。

CONSTANT_Methodref_info

類方法引用表,包含引用的任何型別方法的描述資訊,主要包括類資訊索引和名字型別索引。

圖 2. 常量池各表的關係
圖 2. 常量池各表的關係

可以看到,給定任意一個方法的索引,在常量池中找到對應的條目後,可以得到該方法的類索引(class_index)和名字型別索引 (name_and_type_index), 進而得到該方法所屬的型別資訊和名稱及描述符資訊(引數,返回值等)。注意到所有的常量字串都是儲存在 CONSTANT_Utf8_info 中供其他表索引的。

方法表與方法呼叫

方法表是動態呼叫的核心,也是 Java 實現動態呼叫的主要方式。它被儲存於方法區中的型別資訊,包含有該型別所定義的所有方法及指向這些方法程式碼的指標,注意這些具體的方法程式碼可能是被覆寫的方法,也可能是繼承自基類的方法。

如有類定義 Person, Girl, Boy,

清單 1
 class Person { 
 public String toString(){ 
    return "I'm a person."; 
	 } 
 public void eat(){} 
 public void speak(){} 

}

class Boy extends Person{
public String toString(){
return “I’m a boy”;
}
public void speak(){}
public void fight(){}
}

class Girl extends Person{
public String toString(){
return “I’m a girl”;
}
public void speak(){}
public void sing(){}
}

當這三個類被載入到 Java 虛擬機器之後,方法區中就包含了各自的類的資訊。Girl 和 Boy 在方法區中的方法表可表示如下:

圖 3.Boy 和 Girl 的方法表
圖 3.Boy 和 Girl 的方法表

可以看到,Girl 和 Boy 的方法表包含繼承自 Object 的方法,繼承自直接父類 Person 的方法及各自新定義的方法。注意方法表條目指向的具體的方法地址,如 Girl 的繼承自 Object 的方法中,只有 toString() 指向自己的實現(Girl 的方法程式碼),其餘皆指向 Object 的方法程式碼;其繼承自於 Person 的方法 eat() 和 speak() 分別指向 Person 的方法實現和本身的實現。

Person 或 Object 的任意一個方法,在它們的方法表和其子類 Girl 和 Boy 的方法表中的位置 (index) 是一樣的。這樣 JVM 在呼叫例項方法其實只需要指定呼叫方法表中的第幾個方法即可。

如呼叫如下:

清單 2
 class Party{ 
…
 void happyHour(){ 
 Person girl = new Girl(); 
 girl.speak(); 
…
	 } 
 }

當編譯 Party 類的時候,生成 girl.speak()的方法呼叫假設為:

Invokevirtual #12

設該呼叫程式碼對應著 girl.speak(); #12 是 Party 類的常量池的索引。JVM 執行該呼叫指令的過程如下所示:

圖 4. 解析呼叫過程
圖 4. 解析呼叫過程

JVM 首先檢視 Party 的常量池索引為 12 的條目(應為 CONSTANT_Methodref_info 型別,可視為方法呼叫的符號引用),進一步檢視常量池(CONSTANT_Class_info,CONSTANT_NameAndType_info ,CONSTANT_Utf8_info)可得出要呼叫的方法是 Person 的 speak 方法(注意引用 girl 是其基類 Person 型別),檢視 Person 的方法表,得出 speak 方法在該方法表中的偏移量 15(offset),這就是該方法呼叫的直接引用。

當解析出方法呼叫的直接引用後(方法表偏移量 15),JVM 執行真正的方法呼叫:根據例項方法呼叫的引數 this 得到具體的物件(即 girl 所指向的位於堆中的物件),據此得到該物件對應的方法表 (Girl 的方法表 ),進而呼叫方法表中的某個偏移量所指向的方法(Girl 的 speak() 方法的實現)。

介面呼叫

因為 Java 類是可以同時實現多個介面的,而當用介面引用呼叫某個方法的時候,情況就有所不同了。Java 允許一個類實現多個介面,從某種意義上來說相當於多繼承,這樣同樣的方法在基類和派生類的方法表的位置就可能不一樣了。

清單 3
interface IDance{ 
   void dance(); 
 } 

class Person {
public String toString(){
return “I’m a person.”;
}
public void eat(){}
public void speak(){}

}

class Dancer extends Person
implements IDance {
public String toString(){
return “I’m a dancer.”;
}
public void dance(){}
}

class Snake implements IDance{
public String toString(){
return “A snake.”;
}
public void dance(){
//snake dance
}
}

圖 5.Dancer 的方法表(檢視大圖
圖 5.Dancer 的方法表

可以看到,由於介面的介入,繼承自於介面 IDance 的方法 dance()在類 Dancer 和 Snake 的方法表中的位置已經不一樣了,顯然我們無法通過給出方法表的偏移量來正確呼叫 Dancer 和 Snake 的這個方法。這也是 Java 中呼叫介面方法有其專有的呼叫指令(invokeinterface)的原因。

Java 對於介面方法的呼叫是採用搜尋方法表的方式,對如下的方法呼叫

invokeinterface #13

JVM 首先檢視常量池,確定方法呼叫的符號引用(名稱、返回值等等),然後利用 this 指向的例項得到該例項的方法表,進而搜尋方法表來找到合適的方法地址。

因為每次介面呼叫都要搜尋方法表,所以從效率上來說,介面方法的呼叫總是慢於類方法的呼叫的。

				<script>
					(function(){
						function setArticleH(btnReadmore,posi){
							var winH = $(window).height();
							var articleBox = $("div.article_content");
							var artH = articleBox.height();
							if(artH > winH*posi){
								articleBox.css({
									'height':winH*posi+'px',
									'overflow':'hidden'
								})
								btnReadmore.click(function(){
									if(typeof window.localStorage === "object" && typeof window.csdn.anonymousUserLimit === "object"){
										if(!window.csdn.anonymousUserLimit.judgment()){
											window.csdn.anonymousUserLimit.Jumplogin();
											return false;
										}else if(!currentUserName){
											window.csdn.anonymousUserLimit.updata();
										}
									}
									
									articleBox.removeAttr("style");
									$(this).parent().remove();
								})
							}else{
								btnReadmore.parent().remove();
							}
						}
						var btnReadmore = $("#btn-readmore");
						if(btnReadmore.length>0){
							if(currentUserName){
								setArticleH(btnReadmore,3);
							}else{
								setArticleH(btnReadmore,1.2);
							}
						}
					})()
				</script>
				</article>