今天我們來(lái)填坑,在之前的一篇文章深挖?向?qū)ο?a href="http://srfitnesspt.com/v/tag/1315/" target="_blank">編程三?特性 --封裝、繼承、多態(tài)中 我們遺留了一個(gè)問(wèn)題:當(dāng)父類引用指向子類對(duì)象時(shí),JVM是如何知曉調(diào)用的是哪個(gè)子類的方法?
動(dòng)態(tài)綁定和靜態(tài)綁定
我們下文還是用之前文章的例子,簡(jiǎn)單修改一下:
public class ClassTest {
static class Animal {
public void eat(){
System.out.println("動(dòng)物吃飯!");
}
public void work(){
System.out.println("動(dòng)物可以幫助人類干活!");
}
}
static class Cat extends Animal {
public void eat() {
System.out.println("吃魚(yú)");
}
public void sleep() {
System.out.println("貓會(huì)睡懶覺(jué)");
}
}
static class Dog extends Animal {
public void eat() {
System.out.println("吃骨頭");
}
}
public static void main(String[] args) throws Exception {
Animal cat=new Cat();
cat.eat();
cat.work();
//cat.sleep();//此處編譯會(huì)報(bào)錯(cuò)。
}
}
當(dāng)父類引用指向子類對(duì)象時(shí),也就是Animal cat=new Cat();
這個(gè)也叫做向上轉(zhuǎn)型,重寫(xiě)式多態(tài)。
這種多態(tài)其實(shí)是通過(guò) 動(dòng)態(tài)綁定 (dynamic binding)技術(shù)來(lái)實(shí)現(xiàn),是指 在執(zhí)行期間判斷所引用對(duì)象的實(shí)際類型,根據(jù)其實(shí)際的類型調(diào)用其相應(yīng)的方法 。也就是說(shuō),只有程序運(yùn)行起來(lái),你才知道調(diào)用的是哪個(gè)子類的方法。這種多態(tài)可通過(guò)函數(shù)的重寫(xiě)以及向上轉(zhuǎn)型來(lái)實(shí)現(xiàn)。
與動(dòng)態(tài)綁定相對(duì)應(yīng)的就是 靜態(tài)綁定 ,指的是 在JVM解析時(shí)便能夠直接識(shí)別目標(biāo)方法的情況 。網(wǎng)上有些文章說(shuō),重載和靜態(tài)綁定直接掛鉤,這其實(shí)是不完全正確的,筆者舉個(gè)極端的例子:當(dāng)某個(gè)類中的重載方法被它的子類重寫(xiě)時(shí),那它其實(shí)通過(guò)了動(dòng)態(tài)綁定。
重載指的是方法名相同而參數(shù)類型不相同的方法之間的關(guān)系,重寫(xiě)指的是方法名相同并且參數(shù)類型也相同的方法之間的關(guān)系
需要注意的是: 本文一直在說(shuō)程序在運(yùn)行期間發(fā)生的事,而方法調(diào)用在靜態(tài)階段(編譯) 以聲明的靜態(tài)類型為準(zhǔn) ,不管符號(hào)引用指向的是哪個(gè)實(shí)例對(duì)象。編譯成字節(jié)碼再進(jìn)入JVM,進(jìn)行類加載
我們回到剛剛的例子上:cat.eat();
這句的結(jié)果打?。撼贼~(yú)。程序這塊調(diào)用我們子類Cat定義的方法,而不是父類的同名方法。cat.work();
這句的結(jié)果打?。簞?dòng)物可以幫助人類干活!我們上面Cat類沒(méi)有定義work方法,但是卻使用了父類的方法,這是不是很神奇。
其實(shí)此處調(diào)的是父類的同名方法cat.sleep();
這句 編譯器會(huì)提示 編譯報(bào)錯(cuò)。表明:當(dāng)我們當(dāng)子類的對(duì)象作為父類的引用使用時(shí),只能訪問(wèn)子類中和父類中都有的方法,而無(wú)法去訪問(wèn)子類中特有的方法。
雖然向上轉(zhuǎn)型是安全的。但是缺點(diǎn)是:一旦向上轉(zhuǎn)型,子類會(huì)丟失的子類的擴(kuò)展方法,其實(shí)就是 子類中原本特有的方法就不能再被調(diào)用了。所以cat.sleep()
這句會(huì)編譯報(bào)錯(cuò)。
由此我們可以發(fā)現(xiàn)規(guī)律:當(dāng)發(fā)生向上轉(zhuǎn)型,去調(diào)用方法時(shí),首先檢查父類中是否有該方法,如果沒(méi)有,則編譯錯(cuò)誤;如果有,再去調(diào)用子類的同名方法。如果子類沒(méi)有同名方法,會(huì)再次去調(diào)父類中的該方法。這種根據(jù)對(duì)象的實(shí)際類型而不是聲明類型來(lái)選擇并調(diào)用方法的過(guò)程也叫做 動(dòng)態(tài)分派 (Dynamic Dispatch)但如果直接這樣去查找,會(huì)發(fā)生循環(huán)查找,效率較低,為了解決這個(gè)問(wèn)題,虛方法表 就出現(xiàn)了,也就是動(dòng)態(tài)綁定的底層原理。
虛方法表與虛方法
JVM 虛方法表 (Virtual Method Table),也稱為vtable,是動(dòng)態(tài)調(diào)度用來(lái)依次調(diào)用虛方法的一種表結(jié)構(gòu),是一種特殊的 索引表 。
面向?qū)ο缶幊?,?huì)頻繁地觸發(fā) 動(dòng)態(tài)分派 ,如果每次動(dòng)態(tài)分配的過(guò)程都要重新在類的方法 元數(shù)據(jù)中搜索合適的目標(biāo)的方法,就可能影響到執(zhí)行效率,所以JVM選擇了 用空間換取時(shí)間的策略來(lái)實(shí)現(xiàn)動(dòng)態(tài)綁定, 為每個(gè)類生成一張?zhí)摲椒ū?/strong> ,然后直接通過(guò)虛方法表,使用索引來(lái)代替循環(huán)查找,快速定位目標(biāo)方法。
在類加載器與雙親委派機(jī)制一網(wǎng)打盡一文中,我們知道 類的生命周期一般有如下圖有7個(gè)階段,其中階段1-5為類加載過(guò)程,驗(yàn)證、準(zhǔn)備、解析統(tǒng)稱為連接虛方法表會(huì)在類加載的連接階段被創(chuàng)建,JVM掃描類的方法信息,識(shí)別哪些是 虛方法 ,并在虛方法表中儲(chǔ)存其對(duì)應(yīng)的 方法的相關(guān)信息以及這些 方法在虛擬機(jī)內(nèi)存方法區(qū)中的入口地址 。這入口地址就是該方法的虛擬方法表的索引,JVM可以通過(guò)這個(gè)索引地址找到對(duì)應(yīng)的方法。也就是說(shuō),每個(gè)類的對(duì)象都會(huì)擁有自己的虛方法表
那什么是虛方法和非虛方法?
非虛方法:如果方法在編譯期就確定了具體的調(diào)用版本,則這個(gè)版本在運(yùn)行時(shí)是不可變的,這樣的方法稱為非虛方法靜態(tài)方法。比如私有方法,final 方法,實(shí)例構(gòu)造器,父類方法都是非虛方法,除了這些以外都是虛方法
當(dāng)Java中發(fā)生向上轉(zhuǎn)型,呈現(xiàn)重寫(xiě)式多態(tài)時(shí),如果子類沒(méi)有重寫(xiě)父類方法,子類并不會(huì)復(fù)制一份父類的方法到自己的虛方法表中,就會(huì)去父類的虛方法表中查找 目標(biāo)方法
。
子類的重寫(xiě)的方法和父類中的同名方法在字節(jié)碼層面方法索引通常來(lái)說(shuō)是一樣的,如果在子類找到方法eat()
,其索引是0
,發(fā)現(xiàn)不是要調(diào)用的方法后,而是要調(diào)用父類的eat()
,就會(huì)直接去父類方法索引為0
的地方查找,這樣能進(jìn)一步提高查找效率。
JVM方法調(diào)用的指令
從JVM底層來(lái)了解方法調(diào)用,我們還需知曉 在JVM中和方法調(diào)用有關(guān)的指令有5種:
- invokeinterface :調(diào)用接口中的方法,實(shí)際上是在運(yùn)行期決定的,決定到底調(diào)用實(shí)現(xiàn)該接口的哪個(gè)對(duì)象的特定方法。
- invokestatic :調(diào)用靜態(tài)方法。
- invokespecial : 調(diào)用 私有實(shí)例方法 、構(gòu)造器方法;使用super關(guān)鍵詞調(diào)用父類的實(shí)例方法、構(gòu)造器;調(diào)用所實(shí)現(xiàn)接口的default方法
- invokevirtual :調(diào)用 非私有實(shí)例方法 ,也就是虛方法,運(yùn)行期動(dòng)態(tài)查找的過(guò)程。
- invokedynamic : 調(diào)用動(dòng)態(tài)方法,JDK7新加入的一個(gè)虛擬機(jī)指令,相比于之前的四條指令,他們的分派邏輯都是固化在JVM內(nèi)部,而invokedynamic則用于處理新的方法分派:它允許應(yīng)用級(jí)別的代碼來(lái)確定執(zhí)行哪一個(gè)方法調(diào)用,只有在調(diào)用要執(zhí)行的時(shí)候,才會(huì)進(jìn)行這種判斷,從而達(dá)到動(dòng)態(tài)語(yǔ)言的支持。(Invoke dynamic method)
我們javap來(lái)反編譯上文例子生成的class文件ClassTest.class:
public com.zj.ideaprojects.demo.test4.ClassTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object.":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/zj/ideaprojects/demo/test4/ClassTest$Cat
3: dup
4: invokespecial #3 // Method com/zj/ideaprojects/demo/test4/ClassTest$Cat.":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method com/zj/ideaprojects/demo/test4/ClassTest$Animal.eat:()V
12: aload_1
13: invokevirtual #5 // Method com/zj/ideaprojects/demo/test4/ClassTest$Animal.work:()V
16: return
LineNumberTable:
line 30: 0
line 31: 8
line 32: 12
line 34: 16
Exceptions:
throws java.lang.Exception
我們可以發(fā)現(xiàn):Java 中所有非私有實(shí)例方法調(diào)用都會(huì)被編譯成 invokevirtual指令
,而接口方法調(diào)用都會(huì)被編譯成 invokeinterface 指令
。這兩種指令,均屬于Java 虛擬機(jī)中的 虛方法調(diào)用 ,會(huì)進(jìn)行函數(shù)的動(dòng)態(tài)綁定。
invokevirtual指令
在執(zhí)行時(shí),首先在運(yùn)行期確定方法接收者的實(shí)際類型,并不是把常量池中方法的符號(hào)引用(在這里相當(dāng)于常量池里的方法信息)解析到直接引用上就結(jié)束了,而是接著根據(jù)方法接收者的實(shí)際類型來(lái)選擇方法版本,這個(gè)過(guò)程也就是Java多態(tài)的本質(zhì)。
針對(duì)于invokeinterface指令來(lái)說(shuō),虛擬機(jī)會(huì)建立一個(gè)叫做接口方法表的數(shù)據(jù)結(jié)構(gòu)(interface method table,簡(jiǎn)稱itable),和虛方法表類似。
另外,當(dāng)我們了解invokespecial指令,invokestatic指令
時(shí),可以知曉,父類引用在調(diào)用靜態(tài)方法,私有方法或是接口default方法是不會(huì)發(fā)生多態(tài),而是直接調(diào)用聲明類型的方法。
在Java 8中Lambda表達(dá)式和默認(rèn)方法時(shí),底層會(huì)生成和使用 invokedynamic ,很有意思的一個(gè)指令,本文就不詳細(xì)介紹該指令了,以后有機(jī)會(huì)再講講。
小結(jié)
小結(jié)一下,本文主要講解了方法調(diào)用在Java虛擬機(jī)的實(shí)現(xiàn)方式,以及虛方法表在 JVM 方法調(diào)用中充當(dāng)了一個(gè)中介的角色,使得 JVM 能夠?qū)崿F(xiàn)多態(tài)性和動(dòng)態(tài)分派。最后帶大家了解一下JVM常見(jiàn)的方法調(diào)用的指令,Java可不僅僅只有CRUD哦
參考資料:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.2
《Java虛擬機(jī)規(guī)范》
-
封裝
+關(guān)注
關(guān)注
126文章
7653瀏覽量
142472 -
面向?qū)ο?/span>
+關(guān)注
關(guān)注
0文章
64瀏覽量
9969 -
JVM
+關(guān)注
關(guān)注
0文章
157瀏覽量
12188
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論