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

概述

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

運行時棧幀結(jié)構(gòu)

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

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

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

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

局部變量表

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

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

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

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

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

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

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

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

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

沒有回收placeholder所占的內(nèi)存能說得過去妖碉,因為在執(zhí)行g(shù)c()的時候咆瘟,變量placeholder還處于作用域之內(nèi)温鸽,虛擬機自然不敢回收placeholder的內(nèi)存瓤鼻。那我們把代碼修改一下秉版,如下,placeholder的作用域被限制在了花括號之內(nèi)茬祷,從邏輯上將清焕,在執(zhí)行g(shù)c的時候,placeholder已經(jīng)不可能再被訪問了祭犯,但是執(zhí)行了這段程序秸妥,會發(fā)現(xiàn)結(jié)果如下,還是有64MB的內(nèi)存沒有被回收沃粗。

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

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

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

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

//運行結(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ù)組對象的引用最盅。第一次修改中突雪,代碼雖然已經(jīng)離開了placeholder的作用域,但在此之后涡贱,沒有任何對局部變量表的讀寫操作咏删,placeholder原本所占用的Slot還沒有被其他變量所復(fù)用,所以GCRoots一部分的變量表仍然保持著對它的關(guān)聯(lián)盼产。這種關(guān)聯(lián)沒有被及時打斷饵婆,在絕大部分情況下影響都很輕微。但如果遇到一個方法戏售,其后面的代碼有一些耗時很長的操作侨核,而前面又定義了占用大量內(nèi)存、實際上已經(jīng)不再使用的變量灌灾,手動將其設(shè)置為null值(用來代替那句int a=0搓译,把變量對應(yīng)的局部變量表Slot清空)便不見得是一個絕對有意義的操作,這種操作可以作為你一種在極其特殊的情況(對象占用內(nèi)存大锋喜、此方法的棧幀上時間不能被回收些己、方法嗲用次數(shù)達不到JIT的編譯條件)下的奇技來使用。

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

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

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

操作數(shù)棧

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

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

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

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

JVM的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”顾腊,其中所指的“棧”就是操作數(shù)棧挖胃。

動態(tài)鏈接

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

方法返回地址

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

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

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

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

方法調(diào)用

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

解析

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

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

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

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

只要能被invokestatic和invokespecial指令調(diào)用的方法辈双,都可以在解析階段中確定唯一的版本調(diào)用,符合這個條件的有靜態(tài)方法柜砾、私有方法湃望、實例構(gòu)造器、父類方法4類,它們在類加載的時候就會把符號引用解析為該方法的直接引用证芭。這些方法可以稱為非虛方法瞳浦,與之相反,其他方法稱為虛方法(除去final方法)废士。

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

解析調(diào)用一定是個靜態(tài)的過程卿操,在編譯期間就完全確定,在類裝在的解析階段就會把涉及的符號全部轉(zhuǎn)變?yōu)榭纱_定的直接引用孙援,不會延遲到運行期再去完成害淤。而分派(Dispatch)調(diào)用則可能是靜態(tài)的也可能是動態(tài)的,根據(jù)分派依據(jù)的宗量數(shù)可分為單分派和多分派拓售。這兩類分派方式的兩兩組合就構(gòu)成了靜態(tài)單分派窥摄、靜態(tài)多分派、動態(tài)單分派础淤、動態(tài)多分派4種分派組合情況溪王,下面我們再看看虛擬機中的方法分派是如何進行的。

分派

眾所周知值骇,Java是一門面向?qū)ο蟮恼Z言莹菱,因為Java具備面向?qū)ο蟮?個基本特征:繼承、封裝吱瘩、多態(tài)道伟。這里講的分派調(diào)用過程將會揭示多態(tài)性特征的一些最基本的提現(xiàn),如“重載”和“重寫”在JVM中是如何實現(xiàn)的使碾,這里的實現(xiàn)當(dāng)然不是語法上應(yīng)該如何去寫蜜徽,我們關(guān)心的依然是虛擬機如何確定正確的目標(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);
    }
}

這是在考察閱讀者對重載的理解程度票摇,Human man = new Man();中拘鞋,我們吧Human稱為變量的靜態(tài)類型,或者叫做外觀類型矢门,后面的Man叫做變量的時機類型盆色,靜態(tài)類型和實際類型在程序中都可可能發(fā)生一些變化灰蛙,區(qū)別是靜態(tài)類型的變化僅僅在使用時發(fā)生,變量本身的靜態(tài)類型不會被改變隔躲,并且最終的靜態(tài)類型是在編譯期可知的摩梧;而實際類型變化的結(jié)果在運行期才可確定,編譯器在編譯程序的時候并不知道一個對象的實際類型是什么宣旱。例如:

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

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

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

動態(tài)分派

動態(tài)分派和多態(tài)性的另一個重要體現(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();
    }
}

運行結(jié)果:

man say hello
woman say hello
woman say hello

這個運行結(jié)果相信不會出乎任何人的意料回论,我們還是要知道虛擬機如何調(diào)用到相應(yīng)方法的散罕。這顯然不可能再根據(jù)靜態(tài)類型來決定,因為靜態(tài)類型同樣都是Human的兩個變量man和woman在調(diào)用sayHello()方法時執(zhí)行了不同的行為傀蓉,并且變量man在兩次調(diào)用中執(zhí)行了不同的方法欧漱。導(dǎo)致這個現(xiàn)象的原因很明顯,是這兩個變量的時機類型不同葬燎,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)備動作,作用是建立man和woman的內(nèi)存空間靶草、調(diào)用Man和Woman類型的實例構(gòu)造器蹄胰,將這兩個實例放在第1、2個布局變量表Slot中奕翔,這個動作對應(yīng)了代碼的:

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

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

  1. 找到操作數(shù)棧頂?shù)牡谝粋€元素所指向的對象的實際類型恩急,記作C
  2. 如果在類型C中找到與常量中的描述符合簡單名稱都相符的方法杉畜,則進行訪問權(quán)限校驗,如果通過則返回這個方法的直接引用衷恭,查找過程結(jié)束此叠;如果不通過,則返回java.lang.IllegalAccessError異常
  3. 否則随珠,按照繼承關(guān)系從下往上依次對C的各個父類進行第2步的搜索和驗證過程
  4. 如果始終沒有找到合適的方法灭袁,則拋出java.lang.AbstractMethodError異常。

由于invokevirtual指令執(zhí)行的第一步就是在運行期確定接收者的實際類型窗看,所以兩次調(diào)用invokevirtual指令把常量池中的類方法符號引用解析到了不同的直接引用上茸歧,這個過程就是Java語言中方法重寫的本質(zhì),我們把這種運行期根據(jù)實際類型確定方法執(zhí)行版本的分派過程稱為動態(tài)分派显沈。

單分派與多分派

方法的接收者與方法的參數(shù)統(tǒng)稱為方法的宗量软瞎,這個定義最早應(yīng)該來源于《Java與模式》一書。根據(jù)分派基于多少種宗量构罗,可以降分派劃分為單分派和多分派兩種铜涉。單分派是根據(jù)一個宗量對目標(biāo)方法進行選擇,多分派是根據(jù)多于一個宗量對目標(biāo)方法進行選擇遂唧。

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());
    }
}

運行結(jié)果:

father choose 360
son choose qq

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

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

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

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

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

虛擬機動態(tài)分派的實現(xiàn)

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

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

虛方法表中存放著各個方法的實際入口地址贩猎。如果某個方法在子類中沒有被重寫,那子類的虛方法表里面的地址入口和父類相同方法的地址入口是一致的萍膛,都指向父類的實現(xiàn)入口吭服。如果子類中重寫了這個方法,子類方法表中的地址將會被替換為指向子類實現(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)具有一樣的索引序號, 這樣當(dāng)類型變換時傻昙,僅需求變更查找的方法表闺骚,就可以從不同的虛方法表中按索引轉(zhuǎn)換出所需的入口地址。

動態(tài)語言支持

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

動態(tài)類型語言

動態(tài)類型語言的關(guān)鍵特征是它的類型檢查的主題過程是在運行期而不是編譯期碰镜,滿足這個特性的語言有很多,包括:APL习瑰、Clojure绪颖、Erlang、Groovy杰刽、JavaScript菠发、Jython王滤、Lisp、Lua滓鸠、PHP雁乡、Prolog、Python糜俗、Ruby踱稍、Smalltalk和Tel等。相對于悠抹,在編譯期就進行類型檢查過程的語言(比如C++和Java等)就是最常用的靜態(tài)類型語言珠月。

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

這段代碼時可以正常編譯的,但運行的時候會報NegativeArraySizeException異常楔敌。在JVM規(guī)范中明確規(guī)定了NegativeArraySizeException是一個運行時異常啤挎,通俗一點講,運行時異常就是只要代碼不運行到這一行就不會有問題卵凑。與運行時異常對應(yīng)的就是連接時異常庆聘,即使會導(dǎo)致連接時異常的代碼放在一條無法執(zhí)行到的分支路徑上,類加載時(Java的連接過程不在編譯階段勺卢,而在類加載階段)也照樣會跑出異常伙判。

不過C語言會在編譯期報錯:

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

動態(tài)和靜態(tài)類型語言誰更先進呢蜕提?這個不會有確切的答案。

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

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

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

java.lang.invoke包

JDK1.7實現(xiàn)了JSK-292署隘,新加入的java.lang.invoke包就是JSR-292的一個重要組成部分,這個包的目的是在之前單純依靠符號引用來確定調(diào)用的目標(biāo)方法這種方式以外亚隙,提供一種新的動態(tài)確定目標(biāo)方法的機制磁餐,稱為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最終是哪個實現(xiàn)類,下面這句都能正確調(diào)用到println方法
        getPrintlnMH(obj).invokeExact("sss");
    }

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

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

void sort(List list, MethodHandle methodHandle)

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

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

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

invokedynamic指令

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

每一處含有invokedynamic指令的位置都稱作“動態(tài)調(diào)用點”(Dynamic CallSite)白魂,這條指令的第一個參數(shù)不再是代表方法符號應(yīng)用的CONSTANT_Methodref_info常量,而是變?yōu)镴DK1.7新加入的CONSTANT_InvokeDynamic_info常量上岗,從這個新常量中可以得到3項信息:引導(dǎo)方法福荸、方法類型和名稱。引導(dǎo)方法是由固定的參數(shù)肴掷,并且返回值是java.long.invoke.CallSite對象敬锐,這個代表正要執(zhí)行的目標(biāo)方法調(diào)用。根據(jù)CONSTANT_InvokeDynamic_info常量中提供的信息呆瞻,虛擬機可以找到并且執(zhí)行引導(dǎo)方法台夺,從而獲得一個CallSite對象,最終調(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虛擬機之上的動態(tài)語言赞赖,因此僅僅依靠Java語言的編譯器Javac沒有辦法生成invokedynamic指令的字節(jié)碼滚朵,曾經(jīng)有一個java.dyn.InvokeDynamic的語法糖可以實現(xiàn),后來被取消了前域,所以要使用Java語言來演示invokedynamic指令只能用一些變通的辦法辕近。

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

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

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

在JDK1.7之前吞杭,使用純粹的Java語言很難處理這個問題,直接生成字節(jié)碼就很簡單变丧,如使用ASM等字節(jié)碼工具芽狗,原因在于子類方法無法獲取一個實際類型是祖類的對象引用,而invokevirtual指令的分派邏輯就是按照方法接收者的時機類型進行分派痒蓬,這個邏輯是固化在虛擬機中的童擎,程序員無法改變」ド梗可以使用如下邏輯解決這個問題顾复。

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)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鲁捏,隨后出現(xiàn)的幾起案子芯砸,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,423評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件假丧,死亡現(xiàn)場離奇詭異双揪,居然都是意外死亡,警方通過查閱死者的電腦和手機包帚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,147評論 2 385
  • 文/潘曉璐 我一進店門渔期,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人渴邦,你說我怎么就攤上這事疯趟。” “怎么了谋梭?”我有些...
    開封第一講書人閱讀 157,019評論 0 348
  • 文/不壞的土叔 我叫張陵信峻,是天一觀的道長。 經(jīng)常有香客問我章蚣,道長站欺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,443評論 1 283
  • 正文 為了忘掉前任纤垂,我火速辦了婚禮矾策,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘峭沦。我一直安慰自己贾虽,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,535評論 6 385
  • 文/花漫 我一把揭開白布吼鱼。 她就那樣靜靜地躺著蓬豁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪菇肃。 梳的紋絲不亂的頭發(fā)上地粪,一...
    開封第一講書人閱讀 49,798評論 1 290
  • 那天,我揣著相機與錄音琐谤,去河邊找鬼蟆技。 笑死,一個胖子當(dāng)著我的面吹牛斗忌,可吹牛的內(nèi)容都是我干的质礼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,941評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼织阳,長吁一口氣:“原來是場噩夢啊……” “哼眶蕉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起唧躲,我...
    開封第一講書人閱讀 37,704評論 0 266
  • 序言:老撾萬榮一對情侶失蹤造挽,失蹤者是張志新(化名)和其女友劉穎碱璃,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刽宪,經(jīng)...
    沈念sama閱讀 44,152評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡厘贼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,494評論 2 327
  • 正文 我和宋清朗相戀三年界酒,在試婚紗的時候發(fā)現(xiàn)自己被綠了圣拄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,629評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡毁欣,死狀恐怖庇谆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凭疮,我是刑警寧澤饭耳,帶...
    沈念sama閱讀 34,295評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站执解,受9級特大地震影響寞肖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜衰腌,卻給世界環(huán)境...
    茶點故事閱讀 39,901評論 3 313
  • 文/蒙蒙 一新蟆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧右蕊,春花似錦琼稻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至萝风,卻和暖如春嘀掸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背规惰。 一陣腳步聲響...
    開封第一講書人閱讀 31,978評論 1 266
  • 我被黑心中介騙來泰國打工睬塌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人卿拴。 一個月前我還...
    沈念sama閱讀 46,333評論 2 360
  • 正文 我出身青樓衫仑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親堕花。 傳聞我的和親對象是個殘疾皇子文狱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,499評論 2 348

推薦閱讀更多精彩內(nèi)容