虛擬機(jī)字節(jié)碼執(zhí)行引擎

概述

執(zhí)行引擎是Java虛擬機(jī)最核心的組成部分之一?!疤摂M機(jī)”是一個(gè)相對(duì)于“物理機(jī)”的概念,這兩個(gè)機(jī)器都有代碼執(zhí)行能力,其區(qū)別是物理機(jī)的執(zhí)行引擎是直接建立在處理器、硬件、指令集和操作系統(tǒng)層面的,而虛擬機(jī)的執(zhí)行引擎則是由自己實(shí)現(xiàn)的,因此可以自行指定指令集與執(zhí)行引擎結(jié)構(gòu),并且能夠執(zhí)行那些不被硬件直接支持的指令集格式。 在Java虛擬機(jī)規(guī)范中指定了虛擬機(jī)字節(jié)碼執(zhí)行引擎的概念模型,這個(gè)概念模型成為各種虛擬機(jī)執(zhí)行引擎的統(tǒng)一外觀(Facade)。在不同的虛擬機(jī)實(shí)現(xiàn)里面,執(zhí)行引擎在執(zhí)行Java代碼的時(shí)候可能會(huì)有解釋執(zhí)行(通過解釋器執(zhí)行)和編譯執(zhí)行(通過即時(shí)編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇,也可能兩者兼?zhèn)洌踔吝€可能會(huì)包含幾個(gè)不同級(jí)別的編譯器執(zhí)行引擎。但從外觀上看起來,所有的JVM的的執(zhí)行引擎都是一致的:輸入的是字節(jié)碼文件,處理過程是字節(jié)碼解析的等效過程,輸出的是執(zhí)行結(jié)果,下面主要從概念模型的角度來講解虛擬機(jī)的方法調(diào)用和字節(jié)碼執(zhí)行。

運(yùn)行時(shí)棧幀結(jié)構(gòu)

棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的虛擬機(jī)棧(Virtual Machine Stack)的棧元素。棧幀存儲(chǔ)了方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)連接和方法返回地址等信息。每個(gè)方法從調(diào)用開始至執(zhí)行完成的過程,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里面從入棧到出棧的過程。

每一個(gè)棧幀都包括了局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法返回地址和一些額外的附加信息。在編譯程序代碼的時(shí)候,棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫入到方法表的Code屬性之中,因此一個(gè)棧幀需要分配多少內(nèi)存,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)。

一個(gè)線程中的方法調(diào)用的方法調(diào)用鏈可能會(huì)很長,很多方法都同時(shí)處于執(zhí)行狀態(tài)。對(duì)于執(zhí)行引擎來說,在活動(dòng)線程中,只有位于棧頂?shù)臈攀怯行У模Q為當(dāng)前棧幀(Current Stack Frame),與這個(gè)棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法(Current Method)。執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作,在概念模型上,典型的棧幀結(jié)構(gòu)如圖所示:

棧幀的概念結(jié)構(gòu)

局部變量表

局部變量表(Local Variable Table)是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)容定義的局部變量。在Java程序編譯為Class文件時(shí),就在方法的Code屬性的max_locals數(shù)據(jù)項(xiàng)中確定了該方法所需要分配的局部變量表的最大容量。

局部變量表的容量以變量槽(Variable Slot,下面稱Slot)為最小單位,虛擬機(jī)規(guī)范中并沒有明確指明了一個(gè)Slot應(yīng)占用的內(nèi)存空間大小,只是很有導(dǎo)向性地說到每個(gè)Slot都應(yīng)該能存放一個(gè)boolean、byte、char、short、int、float、reference或returnAddress類型的數(shù)據(jù),這8中數(shù)據(jù)類型,都可以使用32位或更小的內(nèi)存空間來存放,但這種描述與明確指出“每個(gè)Slot占用32位長度的內(nèi)存空間”是有一些差別的,它允許Slot的長度可以隨著處理器、操作系統(tǒng)或虛擬機(jī)的不同而發(fā)生變化。只要保證64位虛擬機(jī)中使用了64位的物理地址空間去實(shí)現(xiàn)一個(gè)Slot,虛擬機(jī)仍要使用對(duì)齊和補(bǔ)白的手段讓Slot在外觀上看起來與32位虛擬機(jī)中的一致。

既然前面提到了JVM的數(shù)據(jù)類型,在此再簡單介紹一下它們。一個(gè)Slot可以存放32位以內(nèi)的數(shù)據(jù)類型,Java中占用32位以內(nèi)的數(shù)據(jù)類型有boolean、byte、char、short、int、float、reference和returnAddress8種類型。第7種reference類型表示對(duì)一個(gè)對(duì)象實(shí)例的引用,虛擬機(jī)規(guī)范既沒有說明它的長度,也沒有說明指出這種引用應(yīng)有怎樣的結(jié)構(gòu)。但一般來說,虛擬機(jī)實(shí)現(xiàn)至少都應(yīng)當(dāng)能通過這個(gè)引用做到兩點(diǎn),一是從此引用中直接或間接地查找到對(duì)象在Java堆中的數(shù)據(jù)存放的起始地址索引,二是此引用中直接或間接地查找到對(duì)象所屬數(shù)據(jù)類型在方法區(qū)中的存儲(chǔ)的類型信息,否則無法實(shí)現(xiàn)Java語言規(guī)范中定義的語法約束。第8種即returnAddress類型目前已經(jīng)很少見了,它是為字節(jié)碼指令jsr、jsr_w和ret服務(wù)的,指向了一條字節(jié)碼指令的地址,很古老的JVM曾經(jīng)使用這幾條指令來實(shí)現(xiàn)異常處理,現(xiàn)在已經(jīng)由異常表代替。

對(duì)于64位的數(shù)據(jù)類型,虛擬機(jī)會(huì)以高位對(duì)齊的方式為其分配兩個(gè)連續(xù)的Slot空間。Java語言中明確的(reference類型則可能是32位也可能是64位)64位的數(shù)據(jù)類型只有l(wèi)ong和double兩種。值得一提的是,這里把long和double數(shù)據(jù)類型分割存儲(chǔ)的做法與“l(fā)ong和double的非原子性協(xié)定”中把一次long和double數(shù)據(jù)類型分割存儲(chǔ)為兩次32位讀寫的做法有些類似。不過,由于局部變量表建立在線程的堆棧上,是線程私有的數(shù)據(jù),無論讀寫兩個(gè)連續(xù)的Slot是否為原子操作,都不會(huì)引起數(shù)據(jù)安全問題。

虛擬機(jī)通過索引定位的方式使用局部變量表,索引值的范圍是從0開始至局部變量表最大的Slot數(shù)量。如果訪問的是32位數(shù)據(jù)類型的變量,索引n就代表了使用第n個(gè)Slot,如果是64位數(shù)據(jù)類型的變量,則說明會(huì)同時(shí)使用n和n+1兩個(gè)Slot。對(duì)于兩個(gè)相鄰的共同存放在一個(gè)64位數(shù)據(jù)的兩個(gè)Slot,不允許采用任何方式單獨(dú)訪問其中的某一個(gè),JVM規(guī)范中明確要求了如果遇到進(jìn)行這種操作的字節(jié)碼序列,虛擬機(jī)應(yīng)該在加載的校驗(yàn)階段拋出異常。

在方法執(zhí)行時(shí),虛擬機(jī)就使用局部變量表完成參數(shù)值變量列表的傳遞過程的,如果執(zhí)行的是實(shí)例方法,那局部變量表中第0位所以的Slot默認(rèn)是用于傳遞方法所屬對(duì)象實(shí)例的引用,在方法中可以通過關(guān)鍵字this來訪問到這個(gè)隱藏的參數(shù)。其余參數(shù)則按照參數(shù)表順序排列,占用從1開始的局部變量Slot,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的Slot。

為了盡量節(jié)省棧空間,局部變量表中的Slot是可以重用的,方法體中定義的變量,其作用域并不一定會(huì)覆蓋整個(gè)方法體,如果,如果當(dāng)前字節(jié)碼PC計(jì)數(shù)器的值已經(jīng)超出了整個(gè)變量的作用域,那這個(gè)變量對(duì)應(yīng)的Slot就可以交給其他變量使用。不過,這樣的設(shè)計(jì)除了節(jié)省棧幀空間之外,還會(huì)伴隨著一些額外的副作用,例如,在某些情況下,Slot的復(fù)用會(huì)直接影響到系統(tǒng)的垃圾收集行為。代碼中向內(nèi)存中填充了64MB的數(shù)據(jù),然后通知虛擬機(jī)進(jìn)行垃圾回收,需要在虛擬機(jī)參數(shù)上加入“-verbose:gc”來觀察垃圾收集的過程。

public static void main(String[] args) {
    byte[] placeholder = new byte[64 * 1024 * 1024];
    System.gc();
}

//運(yùn)行結(jié)果如下
[GC (System.gc())  91791K->66264K(1256448K), 0.0275929 secs]
[Full GC (System.gc())  66264K->66167K(1256448K), 0.0060836 secs]

沒有回收placeholder所占的內(nèi)存能說得過去,因?yàn)樵趫?zhí)行g(shù)c()的時(shí)候,變量placeholder還處于作用域之內(nèi),虛擬機(jī)自然不敢回收placeholder的內(nèi)存。那我們把代碼修改一下,如下,placeholder的作用域被限制在了花括號(hào)之內(nèi),從邏輯上將,在執(zhí)行g(shù)c的時(shí)候,placeholder已經(jīng)不可能再被訪問了,但是執(zhí)行了這段程序,會(huì)發(fā)現(xiàn)結(jié)果如下,還是有64MB的內(nèi)存沒有被回收。

public static void main(String[] args) {
    {
        byte[] placeholder = new byte[64 * 1024 * 1024];
    }
    System.gc();
}

//運(yùn)行結(jié)果如下
[GC (System.gc())  91791K->66264K(1256448K), 0.0275929 secs]
[Full GC (System.gc())  66264K->66167K(1256448K), 0.0060836 secs]

再改成如下代碼,運(yùn)行后發(fā)現(xiàn)內(nèi)存反而被正常回收了:

public static void main(String[] args) {
    {
        byte[] placeholder = new byte[64 * 1024 * 1024];
    }
    int a = 0;
    System.gc();
}

//運(yùn)行結(jié)果如下
[GC (System.gc())  91791K->752K(1256448K), 0.0009265 secs]
[Full GC (System.gc())  752K->631K(1256448K), 0.0046714 secs]

之前的placeholder沒有被回收的根本原因是:局部變量表中的Slot是否存在有關(guān)于placeholder數(shù)組對(duì)象的引用。第一次修改中,代碼雖然已經(jīng)離開了placeholder的作用域,但在此之后,沒有任何對(duì)局部變量表的讀寫操作,placeholder原本所占用的Slot還沒有被其他變量所復(fù)用,所以GCRoots一部分的變量表仍然保持著對(duì)它的關(guān)聯(lián)。這種關(guān)聯(lián)沒有被及時(shí)打斷,在絕大部分情況下影響都很輕微。但如果遇到一個(gè)方法,其后面的代碼有一些耗時(shí)很長的操作,而前面又定義了占用大量內(nèi)存、實(shí)際上已經(jīng)不再使用的變量,手動(dòng)將其設(shè)置為null值(用來代替那句int a=0,把變量對(duì)應(yīng)的局部變量表Slot清空)便不見得是一個(gè)絕對(duì)有意義的操作,這種操作可以作為你一種在極其特殊的情況(對(duì)象占用內(nèi)存大、此方法的棧幀上時(shí)間不能被回收、方法嗲用次數(shù)達(dá)不到JIT的編譯條件)下的奇技來使用。

雖然在前面的代碼中,將對(duì)象賦值為null是有用的,但是不應(yīng)當(dāng)對(duì)賦null值的操作有過多的依賴,更沒有必要把它當(dāng)做一個(gè)普遍的編碼規(guī)范來推廣。原因有兩點(diǎn),從編碼的角度講,以恰當(dāng)?shù)淖兞孔饔糜騺砜刂谱兞炕厥諘r(shí)間才是最優(yōu)雅的解決方法。更關(guān)鍵的是,從執(zhí)行的角度講使用賦null值的操作來優(yōu)化內(nèi)存回收是建立在對(duì)字節(jié)碼執(zhí)行引擎概念模型的理解之上的。在虛擬機(jī)使用解釋器執(zhí)行時(shí),通常與概念模型還比較接近,但經(jīng)過JIT編譯器后,才是虛擬機(jī)執(zhí)行代碼的主要方式,賦null值的操作在經(jīng)過JIIT編譯優(yōu)化后就會(huì)被消除掉,這時(shí)候?qū)⒆兞吭O(shè)置為null就沒有什么意義了。字節(jié)碼被編譯為本地代碼后,對(duì)GC Roots的枚舉也與解釋執(zhí)行期間有巨大差別,以前面的例子來看,第二種方式在gc()執(zhí)行時(shí)就可以正確的回收掉內(nèi)存,無須寫成第三種方式。

關(guān)于局部變量表,還有一點(diǎn)可能會(huì)對(duì)實(shí)際開發(fā)產(chǎn)生影響,就是局部變量不像前面介紹的類變量那樣存在“準(zhǔn)備階段”,我們已經(jīng)知道類變量有兩次賦初始值的過程,一次在準(zhǔn)備階段,賦予系統(tǒng)初始值;另一次在初始化階段,賦予程序員定義的初始值。因此,即使在初始化階段程序員沒有為類變量賦予值也沒有關(guān)系,類變量仍然具有一個(gè)確定的初始值。但局部變量就不一樣,如果一個(gè)局部變量定義了但沒有賦初始值就不能使用的,不要認(rèn)為Java在任何情況下都存在整型變量默認(rèn)為0,布爾值變量默認(rèn)為false等這樣的情況,下面的代碼時(shí)無法被執(zhí)行的,還好編譯器能在編譯期間檢查到這一點(diǎn)并提示,即使編譯器能通過或者手動(dòng)生成字節(jié)碼的方式制造出下面的代碼,字節(jié)碼校驗(yàn)的時(shí)候也會(huì)被虛擬機(jī)愛發(fā)現(xiàn)而導(dǎo)致加載失敗。

public static void main(String[] args) {
    int a;
    System.out.println(a);
}

操作數(shù)棧

操作數(shù)棧也常成為操作站,它是一個(gè)后入先出的棧。同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時(shí)候?qū)懭氲紺ode屬性的max_stacks數(shù)據(jù)項(xiàng)中。操作數(shù)棧的每一個(gè)元素可以是任意的Java數(shù)據(jù)類型,包括long和double。32位數(shù)據(jù)類型所占用的棧容量為1,64位數(shù)據(jù)類型占用的棧容量為2。在方法執(zhí)行的時(shí)候,操作數(shù)棧的深度都不會(huì)超過在max_stacks數(shù)據(jù)項(xiàng)中設(shè)定的最大值。

當(dāng)一個(gè)方法剛剛開始執(zhí)行的時(shí)候,這個(gè)方法的操作數(shù)棧是空的,在方法的執(zhí)行過程中,會(huì)有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容,也就是出棧入棧操作。例如,在做算術(shù)運(yùn)算的時(shí)候是通過操作數(shù)棧來進(jìn)行的,又或者在調(diào)用其他方法的時(shí)候是通過操作數(shù)棧來進(jìn)行參數(shù)傳遞的。

操作數(shù)棧中元素的數(shù)據(jù)類型必須與字節(jié)碼指令的序列嚴(yán)格匹配,在編譯程序代碼的時(shí)候,編譯器要嚴(yán)格保證這一點(diǎn),在類校驗(yàn)階段的數(shù)據(jù)流分析中還要再次檢驗(yàn)這一點(diǎn)。再以iadd指令為例,這個(gè)指令用于整型數(shù)加法,它在執(zhí)行時(shí),最接近棧頂?shù)膬蓚€(gè)元素的數(shù)據(jù)類型必須為int類型,不能出現(xiàn)一個(gè)long和一個(gè)float使用iadd命令相加的情況。

另外,在概念模型中,兩個(gè)棧幀作為虛擬機(jī)棧的元素,是完全相互獨(dú)立的。但是在大多數(shù)虛擬機(jī)的實(shí)現(xiàn)里偶讀會(huì)做一些優(yōu)化處理,令兩個(gè)棧幀出現(xiàn)一部分重疊。讓下面的棧幀的部分操作數(shù)棧與上面棧幀的部分局部變量表重疊在一起。這樣在進(jìn)行方法調(diào)用時(shí)就可以共用一部分?jǐn)?shù)據(jù),無須進(jìn)行額外的參數(shù)復(fù)制傳遞。

JVM的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“?!本褪遣僮鲾?shù)棧。

動(dòng)態(tài)鏈接

每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,持有這個(gè)引用的是為了支持方法調(diào)用過程中的動(dòng)態(tài)鏈接。我們知道Class文件的常量池中存在大量的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用作為參數(shù)。這些符號(hào)引用一部分會(huì)在類加載階段或者第一次使用的時(shí)候就轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。另外一部分將在每一次運(yùn)行期間轉(zhuǎn)化為直接引用,這部分為動(dòng)態(tài)鏈接。關(guān)于這兩個(gè)轉(zhuǎn)化過程的詳細(xì)信息,將在下面進(jìn)行闡述。

方法返回地址

當(dāng)一個(gè)方法開始執(zhí)行后,只有兩種方式可以退出這個(gè)方法:

  • 第一種方式是執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的方法稱為調(diào)用者),是否有返回值和返回值的類型將根據(jù)遇到何方法返回指令來決定,這種退出方法的方式稱為正常完成出口。
  • 另一種退出方式是,在方法執(zhí)行過程中遇到異常,并且這個(gè)異常沒有在方法體中得到處理,無論是JVM內(nèi)部產(chǎn)生的異常,還是代碼中使用athrow字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會(huì)導(dǎo)致方法退出,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個(gè)方法使用異常完成出口的方式退出,是不會(huì)給它的上層調(diào)用者產(chǎn)生任何返回值的。

無論采用哪種方式退出,在方法退出之后,都需要回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,方法返回時(shí)可能需要在棧幀保存一些信息,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)。一般來說,方法正常退出時(shí),調(diào)用者的PC計(jì)數(shù)器的值可以作為返回地址,棧幀中很可能會(huì)保存這個(gè)計(jì)數(shù)器值。而方法異常退出時(shí),返回地址是要通過異常處理器表來確定的,棧幀中一般不會(huì)保存這部分信息。

方法退出的過程實(shí)際上就等于把當(dāng)前棧幀出棧,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓入調(diào)用者操作數(shù)棧中,調(diào)整PC計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令等。

方法調(diào)用

方法調(diào)用不等同于方法執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定被調(diào)用方法的版本,即調(diào)用哪一個(gè)方法,暫時(shí)還不涉及方法內(nèi)部的具體運(yùn)行過程。在程序運(yùn)行時(shí),運(yùn)行方法調(diào)用是最普遍、最頻繁的操作,但前面已經(jīng)講過,Class文件的編譯過程不包含傳統(tǒng)編譯中的連續(xù)步驟,一切方法調(diào)用在Class文件里面存儲(chǔ)的都是符號(hào)引用,而不是方法在實(shí)際運(yùn)行時(shí)內(nèi)存布局中的入口地址(相當(dāng)于之前說的直接引用)。這個(gè)特性給Java帶來了更強(qiáng)大的動(dòng)態(tài)擴(kuò)展能力,但也使得Java方法調(diào)用過程變的相對(duì)復(fù)雜起來,需要在類加載期間,甚至到運(yùn)行期間才能確定目標(biāo)方法的直接引用。

解析

所有方法調(diào)用的目標(biāo)方法在Class文件里面都是一個(gè)常量池中的符號(hào)引用,在類加載的解析階段,會(huì)將其中的一部分符號(hào)引用轉(zhuǎn)化為直接引用,這種解析能成立的前提是:方法在程序真正運(yùn)行之前就有一個(gè)可確定的調(diào)用版本,并且這個(gè)方法的調(diào)用版本在運(yùn)行期是不可改變的。換句話說,調(diào)用目標(biāo)在程序代碼寫好、編譯器進(jìn)行編譯時(shí)就必須確定下來。這類方法的調(diào)用稱為解析(Resolution)。

在Java語言中符合“編譯期可知,運(yùn)行期不變”這個(gè)要求的方法,主要包括靜態(tài)方法和私有方法兩大類,前者與類型直接關(guān)聯(lián),后者在外部不可被訪問,這兩種方法各自的特點(diǎn)決定了它們都不可能通過繼承或別的方式重寫其他版本,因此它們都不適合在類加載階段進(jìn)行解析。

與之相應(yīng)的是,在Java虛擬機(jī)里面提供了5條方法調(diào)用字節(jié)碼指令,如下:

  • invokestatic:調(diào)用靜態(tài)方法
  • invokespecial:調(diào)用實(shí)例構(gòu)造器init方法、私有方法和父類方法
  • involevirtual:調(diào)用所有的虛方法
  • invokeinterface:調(diào)用接口方法,會(huì)在運(yùn)行時(shí)再確定一個(gè)實(shí)現(xiàn)此接口的對(duì)象
  • invokedynamic:在運(yùn)行時(shí)動(dòng)態(tài)解析出調(diào)用點(diǎn)限定符所引用的方法,然后再執(zhí)行該方法,在此之前的4條調(diào)用指令,分派邏輯是固化在JVM內(nèi)部的,而invokedynamic指令的分派邏輯是由用戶所設(shè)定的引導(dǎo)方法決定的。

只要能被invokestatic和invokespecial指令調(diào)用的方法,都可以在解析階段中確定唯一的版本調(diào)用,符合這個(gè)條件的有靜態(tài)方法、私有方法、實(shí)例構(gòu)造器、父類方法4類,它們?cè)陬惣虞d的時(shí)候就會(huì)把符號(hào)引用解析為該方法的直接引用。這些方法可以稱為非虛方法,與之相反,其他方法稱為虛方法(除去final方法)。

Java中的非虛方法除了使用了invokestatic、invokespecial調(diào)用的方法之外還有一種,就是被final修飾的方法,雖然final方法是使用invokevirtual指令來調(diào)用,但是由于它無法被覆蓋,沒有其他版本,所以也無須對(duì)方法接收者進(jìn)行多態(tài)選擇,又或者說多態(tài)選擇的結(jié)果肯定是唯一的。在Java語言規(guī)范中明確說明了final方法是一種非虛方法。

解析調(diào)用一定是個(gè)靜態(tài)的過程,在編譯期間就完全確定,在類裝在的解析階段就會(huì)把涉及的符號(hào)全部轉(zhuǎn)變?yōu)榭纱_定的直接引用,不會(huì)延遲到運(yùn)行期再去完成。而分派(Dispatch)調(diào)用則可能是靜態(tài)的也可能是動(dòng)態(tài)的,根據(jù)分派依據(jù)的宗量數(shù)可分為單分派和多分派。這兩類分派方式的兩兩組合就構(gòu)成了靜態(tài)單分派、靜態(tài)多分派、動(dòng)態(tài)單分派、動(dòng)態(tài)多分派4種分派組合情況,下面我們?cè)倏纯刺摂M機(jī)中的方法分派是如何進(jìn)行的。

分派

眾所周知,Java是一門面向?qū)ο蟮恼Z言,因?yàn)镴ava具備面向?qū)ο蟮?個(gè)基本特征:繼承、封裝、多態(tài)。這里講的分派調(diào)用過程將會(huì)揭示多態(tài)性特征的一些最基本的提現(xiàn),如“重載”和“重寫”在JVM中是如何實(shí)現(xiàn)的,這里的實(shí)現(xiàn)當(dāng)然不是語法上應(yīng)該如何去寫,我們關(guān)心的依然是虛擬機(jī)如何確定正確的目標(biāo)方法。

靜態(tài)分派

閱讀下面代碼:

public class StaticDispatch {
    static abstract class Human {

    }

    static class Man extends Human {

    }

    static class Woman extends Human {

    }

    public void sayHello(Human guy) {
        System.out.println("hello, guy");
    }

    public void sayHello(Man guy) {
        System.out.println("hello, man");
    }

    public void sayHello(Woman guy) {
        System.out.println("hello, woman");
    }


    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch staticDispatch = new StaticDispatch();
        staticDispatch.sayHello(man);
        staticDispatch.sayHello(woman);
    }
}

這是在考察閱讀者對(duì)重載的理解程度,Human man = new Man();中,我們吧Human稱為變量的靜態(tài)類型,或者叫做外觀類型,后面的Man叫做變量的時(shí)機(jī)類型,靜態(tài)類型和實(shí)際類型在程序中都可可能發(fā)生一些變化,區(qū)別是靜態(tài)類型的變化僅僅在使用時(shí)發(fā)生,變量本身的靜態(tài)類型不會(huì)被改變,并且最終的靜態(tài)類型是在編譯期可知的;而實(shí)際類型變化的結(jié)果在運(yùn)行期才可確定,編譯器在編譯程序的時(shí)候并不知道一個(gè)對(duì)象的實(shí)際類型是什么。例如:

//實(shí)際類型變化
Human man = new Man();
man = new Woman();
//靜態(tài)類型
staticDispatch.sayHello((Man) man);
staticDispatch.sayHello((Woman) man);

main里執(zhí)行了兩次sayHello()方法調(diào)用,在方法接收者已經(jīng)確定是對(duì)象staticDispatch的前提下,使用哪個(gè)重載版本,就完全取決于傳入?yún)?shù)的數(shù)量和數(shù)據(jù)類型。代碼中刻意的定義了兩個(gè)靜態(tài)類型相同但實(shí)際類型不同的變量,但虛擬機(jī)(準(zhǔn)確的說是編譯器)在重載的時(shí)候是通過參數(shù)的靜態(tài)類型而不是實(shí)際類型作為判定依據(jù)的。并且靜態(tài)類型是編譯期可知的。因此,在編譯階段,Javac編譯器會(huì)根據(jù)參數(shù)靜態(tài)類型決定使用哪個(gè)重載版本,所以選擇了sayHello(Human)作為調(diào)用目標(biāo),并把這個(gè)方法符號(hào)引用寫到main方法里的兩條invokevirtual指令的參數(shù)中。

所有依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動(dòng)作稱為靜態(tài)分派。靜態(tài)分派的典型方法是方法重載。靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分析的動(dòng)作實(shí)際上不是由虛擬機(jī)來執(zhí)行的。另外,編譯器雖然能確定出方案的重載版本,但在很多情況下這個(gè)重載版本并不是唯一的,往往能確定出方法的重載版本。產(chǎn)生這種模糊結(jié)論的原因是字面量不需要定義,所以字面量沒有顯示的靜態(tài)類型,它的靜態(tài)類型只能通過語言上的規(guī)則去理解和推斷。

動(dòng)態(tài)分派

動(dòng)態(tài)分派和多態(tài)性的另一個(gè)重要體現(xiàn),重寫有著密切的關(guān)聯(lián)。

public class DynamicDispatch {
    static abstract class Human {
        protected abstract void sayHello();
    }

    static class Man extends Human {
        @Override
        protected void sayHello() {
            System.out.println("man say hello");
        }
    }

    static class Woman extends Human {
        @Override
        protected void sayHello() {
            System.out.println("woman say hello");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}

運(yùn)行結(jié)果:

man say hello
woman say hello
woman say hello

這個(gè)運(yùn)行結(jié)果相信不會(huì)出乎任何人的意料,我們還是要知道虛擬機(jī)如何調(diào)用到相應(yīng)方法的。這顯然不可能再根據(jù)靜態(tài)類型來決定,因?yàn)殪o態(tài)類型同樣都是Human的兩個(gè)變量man和woman在調(diào)用sayHello()方法時(shí)執(zhí)行了不同的行為,并且變量man在兩次調(diào)用中執(zhí)行了不同的方法。導(dǎo)致這個(gè)現(xiàn)象的原因很明顯,是這兩個(gè)變量的時(shí)機(jī)類型不同,JVM是如何根據(jù)類型來分派執(zhí)行版本的呢?我們使用javap命令輸出這段代碼的字節(jié)碼,從中尋找答案:

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=3, args_size=1
       0: new           #2                  // class DynamicDispatch$Man
       3: dup
       4: invokespecial #3                  // Method DynamicDispatch$Man."<init>":()V
       7: astore_1
       8: new           #4                  // class DynamicDispatch$Woman
      11: dup
      12: invokespecial #5                  // Method DynamicDispatch$Woman."<init>":()V
      15: astore_2
      16: aload_1
      17: invokevirtual #6                  // Method DynamicDispatch$Human.sayHello:()V
      20: aload_2
      21: invokevirtual #6                  // Method DynamicDispatch$Human.sayHello:()V
      24: new           #4                  // class DynamicDispatch$Woman
      27: dup
      28: invokespecial #5                  // Method DynamicDispatch$Woman."<init>":()V
      31: astore_1
      32: aload_1
      33: invokevirtual #6                  // Method DynamicDispatch$Human.sayHello:()V
      36: return

0~15行的字節(jié)碼是準(zhǔn)備動(dòng)作,作用是建立man和woman的內(nèi)存空間、調(diào)用Man和Woman類型的實(shí)例構(gòu)造器,將這兩個(gè)實(shí)例放在第1、2個(gè)布局變量表Slot中,這個(gè)動(dòng)作對(duì)應(yīng)了代碼的:

    Human man = new Man();
    Human woman = new Woman();

接下來的16~21也是關(guān)鍵部分,16、20句分別把剛剛創(chuàng)建的兩個(gè)對(duì)象的引用壓到棧頂,這兩個(gè)對(duì)象是將要執(zhí)行sayHello方法的所有者,稱為接收者(Receiver);17和21句是方法調(diào)用指令,這兩條調(diào)用指令單從字節(jié)碼角度來看,無論是指令(都是invokevirutal)還是參數(shù)(都是常量池中第22項(xiàng)的常量,注釋顯示了這個(gè)常量是Human.sayhello的符號(hào)引用)完全一樣,但是這兩句指令最終執(zhí)行的目標(biāo)方法并不相同。原因就需要從invokevirtual指令的多態(tài)查找過程開始說起,invokevirtual指令的運(yùn)行時(shí)解析過程大致可以分為以下幾個(gè)步驟:

  1. 找到操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象的實(shí)際類型,記作C
  2. 如果在類型C中找到與常量中的描述符合簡單名稱都相符的方法,則進(jìn)行訪問權(quán)限校驗(yàn),如果通過則返回這個(gè)方法的直接引用,查找過程結(jié)束;如果不通過,則返回java.lang.IllegalAccessError異常
  3. 否則,按照繼承關(guān)系從下往上依次對(duì)C的各個(gè)父類進(jìn)行第2步的搜索和驗(yàn)證過程
  4. 如果始終沒有找到合適的方法,則拋出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)稱為方法的宗量,這個(gè)定義最早應(yīng)該來源于《Java與模式》一書。根據(jù)分派基于多少種宗量,可以降分派劃分為單分派和多分派兩種。單分派是根據(jù)一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇,多分派是根據(jù)多于一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇。

public class Dispatch {
    static class QQ {}

    static class _360 {}

    public static class Father {
        public void hardChoice(QQ arg) {
            System.out.println("father choose qq");
        }

        public void hardChoice(_360 arg) {
            System.out.println("father choose 360");
        }
    }

    public static class Son extends Father {
        public void hardChoice(QQ arg) {
            System.out.println("son choose qq");
        }

        public void hardChoice(_360 arg) {
            System.out.println("son choose 360");
        }
    }

    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new _360());
        son.hardChoice(new QQ());
    }
}

運(yùn)行結(jié)果:

father choose 360
son choose qq

在main函數(shù)中調(diào)用了兩次hardChoice()方法,這兩次調(diào)用的選擇結(jié)果在程序輸出中已經(jīng)顯示的很清楚了。

我們來看看編譯階段編譯器的選擇過程,也就是靜態(tài)分派的過程。這時(shí)選擇目標(biāo)方法的依據(jù)有兩點(diǎn):一是靜態(tài)類型是Father還是Son,二是方法參數(shù)是QQ還是360.這次選擇結(jié)果的最終產(chǎn)物是產(chǎn)生了兩條invokevirtual指令,兩條指令的參數(shù)分別為常量池中指向Father.hardChoice(360)以及Father.hardChoice(QQ)方法的符號(hào)引用。因此是根據(jù)兩個(gè)宗量進(jìn)行選擇,所以Java語言的靜態(tài)分析屬于多分派類型。

再看看運(yùn)行階段虛擬機(jī)的權(quán)責(zé),也就是動(dòng)態(tài)分派的過程。在執(zhí)行son.hardChoice(new QQ());這段代碼時(shí),更準(zhǔn)確的說,是在執(zhí)行這句代碼所對(duì)應(yīng)的invokevirtual指令時(shí),由于編譯期已經(jīng)決定目標(biāo)方法的簽名必須為hardChoice(QQ),虛擬機(jī)此時(shí)不關(guān)心傳遞過來的參數(shù)到底是什么QQ,因?yàn)檫@時(shí)參數(shù)的靜態(tài)類型、實(shí)際類型都對(duì)方法的選擇不會(huì)構(gòu)成任何影響,唯一可以影響虛擬機(jī)選擇的因素只有此方法的接受者的時(shí)機(jī)類型是Father還是Son。因?yàn)橹挥幸粋€(gè)宗量作為選擇依據(jù),所以Java語言的動(dòng)態(tài)分派屬于單分派。

根據(jù)上面的結(jié)論,我們可以總結(jié)一句話:現(xiàn)在的Java語言是一門靜態(tài)多分派、動(dòng)態(tài)單分派的語言。這個(gè)結(jié)論并不是恒久不變的,C#在3.0及之前版本與Java一樣是動(dòng)態(tài)單分派語言,但是在C#4.0中引入了dynamic類型后,就可以很方便的實(shí)現(xiàn)動(dòng)態(tài)多分派。

按照目前Java語言的發(fā)展趨勢(shì),它并沒有直接變?yōu)閯?dòng)態(tài)語言的跡象,而是通過內(nèi)置動(dòng)態(tài)語言(如JavaScript)執(zhí)行引擎的方式來滿足動(dòng)態(tài)性的需求。但是JVM層面上并不是如此的,在JDK1.7中已經(jīng)開始提供對(duì)動(dòng)態(tài)語言的支持了,JDK1.7中新增的invokedynamic指令也成為了最復(fù)雜的一條方法調(diào)用的字節(jié)碼指令,稍后筆者將專門講解這個(gè)JDK1.7的新特性。

虛擬機(jī)動(dòng)態(tài)分派的實(shí)現(xiàn)

前面介紹的分派過程,作為對(duì)虛擬機(jī)概念模型的解析基本上已經(jīng)足夠了,它已經(jīng)解決了虛擬機(jī)在分派中“會(huì)做什么”的這個(gè)問題,但是虛擬機(jī)“具體如何做到”,可能各種虛擬機(jī)實(shí)現(xiàn)會(huì)有差別。

由于動(dòng)態(tài)分派是非常頻繁的動(dòng)作,而且動(dòng)態(tài)分派的方法版本選擇過程需要運(yùn)行時(shí)在類方法元數(shù)據(jù)中搜索合適的目標(biāo)方法,因此在虛擬機(jī)的時(shí)機(jī)實(shí)現(xiàn)中基于性能的考慮,大部分實(shí)現(xiàn)都不會(huì)真正的進(jìn)行如此頻繁的搜索。而面對(duì)這種情況,最常用的穩(wěn)定優(yōu)化手段就是為類在方法區(qū)中建立一個(gè)虛方法表(Virtual Method Table,也叫itable,于此對(duì)應(yīng)的,在invokeinterface執(zhí)行時(shí)也會(huì)用到接口方法表,Interface Method Table,簡稱itable),使用虛方法表索引來代替元數(shù)據(jù)查找以提高性能。

虛方法表中存放著各個(gè)方法的實(shí)際入口地址。如果某個(gè)方法在子類中沒有被重寫,那子類的虛方法表里面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實(shí)現(xiàn)入口。如果子類中重寫了這個(gè)方法,子類方法表中的地址將會(huì)被替換為指向子類實(shí)現(xiàn)版本入口的地址。

Son重寫了來自Father的全部方法,因此Son的方法表沒有指向Father類型數(shù)據(jù)的箭頭。但是Son和Father都沒有重寫來自O(shè)bject的方法,所以它們的方法表中所有從Object繼承來的方法都指向了Object的數(shù)據(jù)類型。

為了程序?qū)崿F(xiàn)上的方便,具有相同簽名的方法,在父類、子類的虛方法表中都應(yīng)當(dāng)具有一樣的索引序號(hào), 這樣當(dāng)類型變換時(shí),僅需求變更查找的方法表,就可以從不同的虛方法表中按索引轉(zhuǎn)換出所需的入口地址。

動(dòng)態(tài)語言支持

JVM的字節(jié)碼指令集的數(shù)量從Sun公司的第一款JVM問世至JDK7來臨之前的十余年時(shí)間里,一直沒有發(fā)生任何變化。隨著JDK7的發(fā)布,字節(jié)碼指令集終于添加了一個(gè)新成員,invokedynamic指令。這條心增加的指令是JDK7實(shí)現(xiàn)“動(dòng)態(tài)類型語言”支持而進(jìn)行的改進(jìn)之一,也是為JDK8可以順利實(shí)現(xiàn)Lambda表達(dá)式做技術(shù)準(zhǔn)備。

動(dòng)態(tài)類型語言

動(dòng)態(tài)類型語言的關(guān)鍵特征是它的類型檢查的主題過程是在運(yùn)行期而不是編譯期,滿足這個(gè)特性的語言有很多,包括:APL、Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk和Tel等。相對(duì)于,在編譯期就進(jìn)行類型檢查過程的語言(比如C++和Java等)就是最常用的靜態(tài)類型語言。

public static void main(String[] args) {
    int[][][] a = new int[1][0][-1];
}

這段代碼時(shí)可以正常編譯的,但運(yùn)行的時(shí)候會(huì)報(bào)NegativeArraySizeException異常。在JVM規(guī)范中明確規(guī)定了NegativeArraySizeException是一個(gè)運(yùn)行時(shí)異常,通俗一點(diǎn)講,運(yùn)行時(shí)異常就是只要代碼不運(yùn)行到這一行就不會(huì)有問題。與運(yùn)行時(shí)異常對(duì)應(yīng)的就是連接時(shí)異常,即使會(huì)導(dǎo)致連接時(shí)異常的代碼放在一條無法執(zhí)行到的分支路徑上,類加載時(shí)(Java的連接過程不在編譯階段,而在類加載階段)也照樣會(huì)跑出異常。

不過C語言會(huì)在編譯期報(bào)錯(cuò):

int main(void) {
    int i[1][0][-1];//GCC拒絕編譯,報(bào)“size of array is negative”
    return 0;
}

動(dòng)態(tài)和靜態(tài)類型語言誰更先進(jìn)呢?這個(gè)不會(huì)有確切的答案。

  • 靜態(tài)類型語言在編譯期確定類型,最顯著的好處是編譯期可以提供嚴(yán)謹(jǐn)?shù)念愋蜋z查,這樣與類型相關(guān)的問題能在編碼的時(shí)候就及時(shí)被發(fā)現(xiàn),利于穩(wěn)定性以及代碼達(dá)到更大的規(guī)模。
  • 動(dòng)態(tài)類型語言在運(yùn)行期確定類型,這可以為開發(fā)者提供更大的靈活性,某些靜態(tài)類型語言中需要大量臃腫代碼來實(shí)現(xiàn)的功能,由動(dòng)態(tài)類型語言來實(shí)現(xiàn)可能更加清晰和簡潔,也就意味著開發(fā)效率的提升。

JDK1.7與動(dòng)態(tài)類型

JDK1.7以前的字節(jié)碼指令集中,4條方法調(diào)用指令(invokevirtual、invokespecial、invokestatic、invokeinterface)的第一個(gè)參數(shù)都是被調(diào)用的方法的符號(hào)引用(CONSTANT_Methodref_info或者CONSTANT_InterfaceMethodref_info常量),方法的符號(hào)引用在編譯時(shí)產(chǎn)生,而動(dòng)態(tài)類型語言只有在運(yùn)行期才能確定接受者類型。這樣,在JVM上實(shí)現(xiàn)的動(dòng)態(tài)類型語言就不得不使用其他方式,比如在編譯時(shí)留個(gè)占位符類型,運(yùn)行時(shí)動(dòng)態(tài)生成字節(jié)碼實(shí)現(xiàn)具體類型到占位符類型的適配來實(shí)現(xiàn),這樣會(huì)讓動(dòng)態(tài)類型語言實(shí)現(xiàn)的復(fù)雜度增加,也可能帶來額外的性能開銷。盡管可以利用一些方法讓這些開銷變小,但這種底層問題終究是應(yīng)當(dāng)在虛擬機(jī)層次上去解決才最合適,因此在JVM層面上提供動(dòng)態(tài)類型的直接支持就稱為了JVM平臺(tái)的發(fā)展趨勢(shì)之一,這就是JDK1.7中invokedynamic指令以及java.lang.invoke包出現(xiàn)的技術(shù)背景。

java.lang.invoke包

JDK1.7實(shí)現(xiàn)了JSK-292,新加入的java.lang.invoke包就是JSR-292的一個(gè)重要組成部分,這個(gè)包的目的是在之前單純依靠符號(hào)引用來確定調(diào)用的目標(biāo)方法這種方式以外,提供一種新的動(dòng)態(tài)確定目標(biāo)方法的機(jī)制,稱為MethodHandle。擁有MethodHandle之后,Java語言也可以擁有類似函數(shù)指針或者委托的方法別名的工具了。

public class MethodHandleTest {
    static class ClassA {
        public void println(String s) {
            System.out.println(s);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
        //無論obj最終是哪個(gè)實(shí)現(xiàn)類,下面這句都能正確調(diào)用到println方法
        getPrintlnMH(obj).invokeExact("sss");
    }

    private static MethodHandle getPrintlnMH(Object receiver) throws Throwable {
        /*MethodType:代表方法類型,包含了方法的返回值methodType()的第一個(gè)參數(shù)和具體參數(shù)methodType()第二個(gè)及以后的參數(shù)。*/
        MethodType mt = MethodType.methodType(void.class, String.class);
        /*lookup()方法的作用是在指定類中查找符合給定的方法名稱、方法類型,并且符合調(diào)用權(quán)限的方法句柄
        因?yàn)檫@里調(diào)用的是一個(gè)虛方法,按照J(rèn)ava語言的規(guī)則,方法第一個(gè)參數(shù)是隱式的,代表該方法的接收者,也即是this指向的對(duì)象,這個(gè)參數(shù)之前是放在參數(shù)列表中傳遞的,而現(xiàn)在提供了bindTo()方法來完成這件事情*/
        return MethodHandles.lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver);
    }
}

實(shí)際上,getPrintlnMH()方法模擬了invokevirtual指令的執(zhí)行過程,只不過它的分派邏輯并非固化在Class文件的字節(jié)碼上,而是通過一個(gè)具體方法來實(shí)現(xiàn)。而這個(gè)方法本身的返回值(MethodHandle對(duì)象),可以視作最終調(diào)用這個(gè)方法的一個(gè)“引用”。以此為基礎(chǔ),有了MethodHandle就可以寫出類似下面的函數(shù)聲明:

void sort(List list, MethodHandle methodHandle)

僅僅站在Java的角度來看,MethodHandle的使用方法和效果與Reflection有眾多相似之處,但是,它們還有以下這些區(qū)別。

  • 從本質(zhì)上講,Reflection和MethodHandle機(jī)制都是在模擬方法調(diào)用,但Reflection是在模擬Java代碼層次的方法調(diào)用,而MethodHandle是在模擬字節(jié)碼層次的方法調(diào)用,在MethodHandles.lookup中的3個(gè)方法——findStatic()、findVirtual()、findSpecial()正是為了對(duì)應(yīng)于invokestatic、invokevirtual & invokeinterface、invokespecial這幾條字節(jié)碼指令的執(zhí)行權(quán)限校驗(yàn)行為,而這些底層細(xì)節(jié)在使用Reflection API時(shí)是不需要關(guān)心的。
  • Reflection中的java.lang.reflect.Method對(duì)象遠(yuǎn)比MethodHandle機(jī)制中的java.lang.invoke.MethodHandle對(duì)象所包含的信息多。前者是方法在Java一端的全面映像,包括方法的簽名、描述符以及方法屬性表中各個(gè)屬性的Java端表示方式,還包含執(zhí)行權(quán)限等運(yùn)行時(shí)信息。而后者僅僅包含與執(zhí)行該方法相關(guān)的信息。用通俗的話講,Reflection是重量級(jí)的,MethodHandle是輕量級(jí)的。
  • 由于MethodHandle對(duì)字節(jié)碼的方法指令調(diào)用的模擬,所以理論上虛擬機(jī)在這方面做的各種優(yōu)化(比如方法內(nèi)聯(lián)),在MethodHandle上也應(yīng)可以采用類似思路去支持(但目前還不完善)。而通過反射區(qū)調(diào)用方法則不行。

MethodHandle和Reflection除了上面列舉的區(qū)別外,最關(guān)鍵的一點(diǎn)在于去掉前面的“僅僅站在Java的角度來看”。Reflection的設(shè)計(jì)目標(biāo)是只為Java語言服務(wù)的,而MethodHandle則設(shè)計(jì)成可以服務(wù)于所有Java虛擬機(jī)之上的語言,其中也包括Java語言。

invokedynamic指令

從某種程度上講,invokedynamic指令與MethodHandle機(jī)制的作用是一樣的,都是為了解決原有4條“invoke*”指令方法分派規(guī)則固化在虛擬機(jī)之中的問題,把如何查找目標(biāo)方法的決定權(quán)從虛擬機(jī)轉(zhuǎn)嫁到具體用戶代碼之中,讓用戶有更高的自由度。而且兩者的思路也是可類比的,可以把它們想象成為了達(dá)到同一個(gè)目的,一個(gè)采用上層Java代碼和API實(shí)現(xiàn),另一個(gè)采用字節(jié)碼和Class中其他屬性、常量來完成。因此,如果理解了MethodHandle,那么理解invokedynamic指令也并不難。

每一處含有invokedynamic指令的位置都稱作“動(dòng)態(tài)調(diào)用點(diǎn)”(Dynamic CallSite),這條指令的第一個(gè)參數(shù)不再是代表方法符號(hào)應(yīng)用的CONSTANT_Methodref_info常量,而是變?yōu)镴DK1.7新加入的CONSTANT_InvokeDynamic_info常量,從這個(gè)新常量中可以得到3項(xiàng)信息:引導(dǎo)方法、方法類型和名稱。引導(dǎo)方法是由固定的參數(shù),并且返回值是java.long.invoke.CallSite對(duì)象,這個(gè)代表正要執(zhí)行的目標(biāo)方法調(diào)用。根據(jù)CONSTANT_InvokeDynamic_info常量中提供的信息,虛擬機(jī)可以找到并且執(zhí)行引導(dǎo)方法,從而獲得一個(gè)CallSite對(duì)象,最終調(diào)用要執(zhí)行的目標(biāo)方法。

import java.lang.invoke.*;

public class InvokeDynamicTest {
    public static void main(String[] args) throws Throwable {
        INDY_BootstrapMethod().invokeExact("icfenix");
    }

    public static void testMethod(String s) {
        System.out.println("hello String:" + s);
    }

    public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable {
        return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt));
    }

    private static MethodType MT_BootstrapMethod() {
        return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", null);
    }

    private static MethodHandle MH_BootstrapMethod() throws Throwable {
        return MethodHandles.lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod());
    }

    private static MethodHandle INDY_BootstrapMethod() throws Throwable {
        CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(MethodHandles.lookup(), "testMethod",
                MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null));
        return cs.dynamicInvoker();
    }
}

這段代碼與前面的MethodHandleTest的作用基本上是一樣的,由于invokedynamic指令所面向的使用者并非是Java語言,而是其他Java虛擬機(jī)之上的動(dòng)態(tài)語言,因此僅僅依靠Java語言的編譯器Javac沒有辦法生成invokedynamic指令的字節(jié)碼,曾經(jīng)有一個(gè)java.dyn.InvokeDynamic的語法糖可以實(shí)現(xiàn),后來被取消了,所以要使用Java語言來演示invokedynamic指令只能用一些變通的辦法。

掌握方法分派規(guī)則

invokedynamic指令與前面的“invoke*”指令的最大差別就是它的分派邏輯不是由虛擬機(jī)決定的,而是由程序員決定的。

在Java程序中,可以通過super關(guān)鍵字很方便的調(diào)用到父類的方法,但是如果要訪問祖類的方法呢?

在JDK1.7之前,使用純粹的Java語言很難處理這個(gè)問題,直接生成字節(jié)碼就很簡單,如使用ASM等字節(jié)碼工具,原因在于子類方法無法獲取一個(gè)實(shí)際類型是祖類的對(duì)象引用,而invokevirtual指令的分派邏輯就是按照方法接收者的時(shí)機(jī)類型進(jìn)行分派,這個(gè)邏輯是固化在虛擬機(jī)中的,程序員無法改變??梢允褂萌缦逻壿嫿鉀Q這個(gè)問題。

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;

public class GrandTest {
    static class GrandFather {
        void thinking() {
            System.out.println("GrandFather");
        }
    }

    static class Father extends GrandFather {
        void thinking() {
            System.out.println("Father");
        }
    }

    static class Son extends Father {
        void thinking() {
            System.out.println("Son");
            try {
                MethodType mt = MethodType.methodType(void.class);
                Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
                IMPL_LOOKUP.setAccessible(true);
                MethodHandles.Lookup lkp = (MethodHandles.Lookup) IMPL_LOOKUP.get(null);
                MethodHandle h1 = lkp.findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
                h1.invoke(this);
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new GrandTest.Son().thinking();
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容