方法調(diào)用的主要任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個(gè)方法),該過程不涉及方法具體的運(yùn)行過程。class文件的編譯過程中不包含傳統(tǒng)編譯中的連接步驟,一切方法調(diào)用在class文件中存儲(chǔ)的都是符號(hào)引用,而不是方法在實(shí)際運(yùn)行時(shí)內(nèi)存布局中的入口地址,這使得java有著更強(qiáng)大的動(dòng)態(tài)擴(kuò)展能力,但也使得java方法的調(diào)用過程變得相對(duì)復(fù)雜起來,需要在類的加載甚至運(yùn)行期間才能確定目標(biāo)方法的直接引用。 按照調(diào)用方式共分為兩類:
解析調(diào)用時(shí)是靜態(tài)的過程,在編譯期間就完全確定目標(biāo)方法。分派調(diào)用則即可能是靜態(tài),也可能是動(dòng)態(tài)的,根據(jù)分派標(biāo)準(zhǔn)可以分為單分派和多分派。兩兩組合有形成了靜態(tài)單分派、靜態(tài)多分派、動(dòng)態(tài)單分派、動(dòng)態(tài)多分派在Class文件中,所有方法調(diào)用中的目標(biāo)方法都是常量池中的符號(hào)引用,在類加載的解析階段,會(huì)將一部分符號(hào)引用轉(zhuǎn)為直接引用,也就是在編譯階段就能夠確定唯一的目標(biāo)方法,這類方法的調(diào)用成為解析調(diào)用。此類方法主要包括靜態(tài)方法和私有方法兩大類,前者與類型直接關(guān)聯(lián),后者在外部不可訪問,因此決定了他們都不可能通過繼承或者別的方式重寫該方法,符合這兩類的方法主要有以下幾種:靜態(tài)方法、私有方法、實(shí)例構(gòu)造器、父類方法。虛擬機(jī)中提供了以下幾條方法調(diào)用指令:
invokestatic:調(diào)用靜態(tài)方法,解析階段確定唯一方法版本invokespecial:調(diào)用<init>方法、私有及父類方法,解析階段確定唯一方法版本invokevirtual:調(diào)用所有虛方法invokeinterface:調(diào)用接口方法invokedynamic:動(dòng)態(tài)解析出需要調(diào)用的方法,然后執(zhí)行前四條指令固化在虛擬機(jī)內(nèi)部,方法的調(diào)用執(zhí)行不可認(rèn)為干預(yù),而invokedynamic指令則支持由用戶確定方法版本。
其中invokestatic指令和invokespecial指令調(diào)用的方法稱為非虛方法,其余的(final修飾的除外)稱為虛方法。
雖然final方法是使用invokevirtual指令來調(diào)用,但是它無法被覆蓋,沒有其他版本,所以也無須對(duì)方法接收者進(jìn)行多態(tài)選擇,又說多態(tài)選擇的結(jié)果肯定是唯一的。
依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動(dòng)作,稱為靜態(tài)分派。靜態(tài)分派的最典型的應(yīng)用就是方法重載。
靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分派的動(dòng)作實(shí)際上不是由虛擬機(jī)來執(zhí)行的。
在運(yùn)行期間根據(jù)實(shí)際類型來確定方法執(zhí)行版本的分派調(diào)用過程稱為動(dòng)態(tài)分派。這跟多態(tài)性的另一個(gè)體現(xiàn)——重寫有著很密切的關(guān)聯(lián)。
分別把剛剛創(chuàng)建的兩個(gè)對(duì)象的引用壓到棧頂,這兩個(gè)對(duì)象是將要執(zhí)行的方法的所有者,稱為接收者;然后是是方法調(diào)用指令,這兩條調(diào)用指令單從字節(jié)碼角度來看,無論是指令(都是invokevirtual)還是參數(shù)完全一樣的,但是這兩句指令最終執(zhí)行的目標(biāo)方法并不相同。
原因就需要從invokevirtual指令的多態(tài)查找過程開始說起,invokevirtual指令的運(yùn)行時(shí)解析過程大致分為以下幾個(gè)步驟:
找到操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象的實(shí)際類型,記作C。如果在類型C中找到與常量中的描述符和簡(jiǎn)單名稱都相符的方法,則進(jìn)行訪問權(quán)限校驗(yàn),如果通過則返回這個(gè)方法的直接引用,查找過程結(jié)束;如果不通過,則返回java.lang.IllegalaccessError異常。否則,按照繼承關(guān)系從下往上依次對(duì)C的各個(gè)父類進(jìn)行第2步的搜索和驗(yàn)證過程。 如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。由于invokevirtual指令執(zhí)行的第一步就是在運(yùn)行期確定接收者的實(shí)際類型,所以兩次調(diào)用中的invokevirtual指令把常量池中的類方法符號(hào)引用解析到了不同的直接引用上,這個(gè)過程就是Java語言中方法重寫的本質(zhì)。我們把這種在運(yùn)行期根據(jù)實(shí)際類型確定方法執(zhí)行版本的分派過程稱為動(dòng)態(tài)分派。
方法的接收者與方法的參數(shù)統(tǒng)稱為方法的宗量。根據(jù)分派基于多少種宗量,可以將分派劃分為單分派和多分派兩種。單分派是根據(jù)一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇,多分派則是根據(jù)多于一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇。
Java語言的靜態(tài)分派屬于多分派類型。 Java語言的動(dòng)態(tài)分派屬于單分派類型。
由于動(dòng)態(tài)分派是非常頻繁的動(dòng)作,而且動(dòng)態(tài)分派的方法版本選擇過程需要運(yùn)行時(shí)在類的方法元數(shù)據(jù)中搜索合適的目標(biāo)方法,因此在虛擬機(jī)的實(shí)際實(shí)現(xiàn)中基于性能的考慮,大部分實(shí)現(xiàn)都不會(huì)真正地進(jìn)行如此頻繁的搜索。面對(duì)這種情況,最常用的“穩(wěn)定優(yōu)化”手段就是為類在方法區(qū)中建立一個(gè)虛方法表(Vritual Method Table,也稱為vtable,與此對(duì)應(yīng)的,在invokeinterface執(zhí)行時(shí)也會(huì)用到接口方法表——Inteface Method Table,簡(jiǎn)稱itable),使用虛方法表索引來代替元數(shù)據(jù)查找以提高性能。
虛方法表中存放著各個(gè)方法的實(shí)際入口地址。如果某個(gè)方法在子類中沒有被重寫,那子類的虛方法表里面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實(shí)現(xiàn)入口。如果子類中重寫了這個(gè)方法,子類方法表中的地址將會(huì)替換為指向子類實(shí)現(xiàn)版本的入口地址。
為了程序?qū)崿F(xiàn)上的方便,具有相同簽名的方法,在父類、子類的虛方法表中都應(yīng)當(dāng)具有一樣的索引序號(hào),這樣當(dāng)類型變換時(shí),僅需要變更查找的方法表,就可以從不同的虛方法表中按索引轉(zhuǎn)換出所需的入口地址。方法表一般在類加載的連接階段進(jìn)行初始化,準(zhǔn)備了類的變量初始值后,虛擬機(jī)會(huì)把該類的方法表也初始化完畢。
ps:方法表是分派調(diào)用的“穩(wěn)定優(yōu)化”手段,虛擬機(jī)除了使用方法表之外,在條件允許的情況下,還會(huì)使用內(nèi)聯(lián)緩存(Inline Cache)和基于“類型繼承關(guān)系分析”(Class Hierarchy Analysis,CHA)技術(shù)的守護(hù)內(nèi)聯(lián)(Guarded Inlining)兩種非穩(wěn)定的“激進(jìn)優(yōu)化”手段來獲得更高的性能,關(guān)于這兩種優(yōu)化技術(shù)的原理和運(yùn)作過程,可以參考JIT晚期運(yùn)行期。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注