每當(dāng)啟動(dòng)一個(gè)線程時(shí),JVM就為它分配一個(gè)Java棧,棧是以幀為單位保存當(dāng)前線程的運(yùn)行狀態(tài)的。某個(gè)線程正在執(zhí)行的方法稱為當(dāng)前方法,當(dāng)前方法使用的幀稱為當(dāng)前幀,當(dāng)前方法所屬的類稱為當(dāng)前類,當(dāng)前類的常量池稱為當(dāng)前常量池。當(dāng)線程執(zhí)行一個(gè)方法時(shí),它會(huì)追蹤當(dāng)前常量池。
棧幀是虛擬機(jī)棧的棧元素,每當(dāng)在調(diào)用一個(gè)方法時(shí),才為當(dāng)前方法分配一個(gè)幀,然后將該幀壓入棧頂,這個(gè)幀就成了當(dāng)前幀,當(dāng)執(zhí)行這個(gè)方法時(shí),它使用這個(gè)幀來存儲(chǔ)參數(shù)、局部變量,中間計(jì)算結(jié)果等。
棧是保存線程運(yùn)行狀態(tài)的,幀是保存方法執(zhí)行的運(yùn)行狀態(tài)的,幀是棧的元素。線程的切換對(duì)應(yīng)著棧的出棧入棧,線程中的不同方法依次運(yùn)行對(duì)應(yīng)著幀的出棧和入棧。
幀的組成部分
1. 局部變量表
局部變量表的大小是編譯期間可知的,因?yàn)樵趈ava程序編譯成class文件的時(shí)候,就在方法表對(duì)應(yīng)方法的code屬性的max_locals數(shù)據(jù)項(xiàng)中確定了該方法所需要分配的局部變量表的最大容量。
局部變量表的容量以變量槽slot為最小單位,一個(gè)slot的大小為32bit,在64位虛擬機(jī)中需要使用對(duì)齊和補(bǔ)白的手段讓slot在外觀上看起來與32位虛擬機(jī)中的一致。對(duì)于64位的數(shù)據(jù)(long,doble)需要占用兩個(gè)slot,對(duì)于這種占兩個(gè)slot的數(shù)據(jù)類型存儲(chǔ),不允許采用任何方式單獨(dú)訪問其中某一個(gè)。
局部變量表順序:變量表從索引0開始,依次存放方法所屬對(duì)象的引用(如果為靜態(tài)方法則沒有)、方法參數(shù)變量(按照聲明順序)、方法內(nèi)局部變量(按照聲明順序)。注意,對(duì)于short、byte、char這三種數(shù)據(jù)類型需要轉(zhuǎn)換成int類型存儲(chǔ)在局部變量表中。
類變量與局部變量:class文件在被JVM加載時(shí),創(chuàng)建Class對(duì)象,分配內(nèi)存空間時(shí)會(huì)為類變量指定初始值。但局部變量定義了沒有賦初始值是不能使用的,會(huì)出現(xiàn)編譯錯(cuò)誤。
slot是可重用的:對(duì)于局部變量中沒有覆蓋整個(gè)方法的作用域的變量是可重用的。對(duì)于可重用的slot,如果后面沒有在定義變量對(duì)這個(gè)slot進(jìn)行覆蓋,即使這個(gè)變量已經(jīng)離開了其作用域(無效),那么這個(gè)變量在方法體內(nèi)也不會(huì)被回收,除非顯示的賦值為null(解釋執(zhí)行的時(shí)候),但是在JIT編譯器優(yōu)化后賦值為null的操作就會(huì)被消除掉,這時(shí)候?qū)⒆兞吭O(shè)置為null就是沒有意義的。
2. 操作數(shù)棧
操作數(shù)棧和局部變量表一樣也是編譯期間可知的,操作數(shù)棧的最大深度在編譯的時(shí)候?qū)懭氲紺ode屬性的max_stacks數(shù)據(jù)項(xiàng)中。操作數(shù)棧的每一個(gè)元素可以是任意java數(shù)據(jù)類型,32位數(shù)據(jù)容量為1,64位數(shù)據(jù)容量為2,在方法執(zhí)行的時(shí)候,操作數(shù)棧的深度不會(huì)超過max_stacks數(shù)據(jù)項(xiàng)中設(shè)定的最大值。
在概念模型中,兩個(gè)棧幀是相互獨(dú)立的,但是在多數(shù)虛擬機(jī)實(shí)現(xiàn)里都會(huì)做一些優(yōu)化處理,令兩個(gè)棧幀出現(xiàn)一部分重疊,讓下面棧幀的部分操作數(shù)棧與上面的棧幀的部分局部變量表重疊在一起,這樣在進(jìn)行方法調(diào)用時(shí)就會(huì)公用一部分?jǐn)?shù)據(jù),無需進(jìn)行額外的參數(shù)復(fù)制。java虛擬機(jī)的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中棧指的就是操作數(shù)棧。
3. 動(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)解析。
4. 方法返回地址
退出方法有兩種方法:
正常完成出口:執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令。
異常完成出口:在方法的執(zhí)行過程中遇到了異常,并且這個(gè)異常沒有在方法體內(nèi)得到處理。
無論以哪種方式退出,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,一般來說,方法正常退出時(shí),調(diào)用者的PC計(jì)數(shù)器的值可以作為返回地址,棧幀中很可能會(huì)保存這個(gè)計(jì)數(shù)器值;方法異常退出時(shí),返回的地址是要通過異常處理器表來確定的,棧幀中一般不會(huì)保存這部分信息。
方法退出相當(dāng)于把當(dāng)前棧幀出棧,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話) 壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整PC計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令。
方法調(diào)用
確定方法調(diào)用的版本,這個(gè)階段并沒有執(zhí)行方法體的內(nèi)容。class文件的編譯過程中不包含傳統(tǒng)編譯中的連接步驟,一切方法調(diào)用在class文件里面存儲(chǔ)的都只是符號(hào)引用,而不是方法在實(shí)際運(yùn)行時(shí)內(nèi)存布局中的入口地址(直接引用),需要在類加載期間,甚至到運(yùn)行期間才能確定目標(biāo)方法的直接引用。
解析調(diào)用
在類加載的解析階段,會(huì)將其中一部分符號(hào)引用轉(zhuǎn)化為直接引用,這種解析的前提是:方法在程序真正執(zhí)行之前就有一個(gè)可確定的調(diào)用版本,并且這個(gè)方法的調(diào)用版本在運(yùn)行期間是不可變的。換句話說,調(diào)用目標(biāo)在程序代碼寫好,編譯器進(jìn)行編譯時(shí)就必須確定下來。這類方法的調(diào)用稱為解析調(diào)用。
在java語言中符合“編譯期間可知,運(yùn)行期間不變”要求的方法主要有:靜態(tài)方法(invokestatic指令調(diào)用)、私有方法(invokespecial指令調(diào)用)、實(shí)例構(gòu)造器方法(invokespecial指令調(diào)用)、父類方法(invokespecial指令調(diào)用)、final方法(invokevirtual指令調(diào)用)。凡是能在解析階段中確定唯一的調(diào)用版本的這些方法稱為非虛方法,與之相反,其他的方法則成為虛方法。分派
2.1. 靜態(tài)分派與多態(tài)之重載
重載 : 方法名相同,方法簽名不同,調(diào)用時(shí)根據(jù)方法的簽名選擇最佳的方法。
虛擬機(jī)(編譯器)在重載時(shí)是通過參數(shù)的靜態(tài)類型而不是實(shí)際類型作為判定依據(jù)的, 并且靜態(tài)類型是編譯期間可知的,因此,在編譯階段,javac編譯器會(huì)根據(jù)參數(shù)的靜態(tài)類型決定使用哪個(gè)重載版本。
靜態(tài)類型不可變,編譯期間可知,實(shí)際類型可變,編譯期間不可知,運(yùn)行期間確定。
//實(shí)際類型變化
Human man = new Man();
man = new Women();
//靜態(tài)類型變化
sr.sayHello((Man) man);
sr.sayHello((Woman) man);
靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分派的動(dòng)作實(shí)際上不是有虛擬機(jī)來執(zhí)行的。很多情況下這個(gè)重載版本并不是“唯一的”,往往只能確定一個(gè)更加合適的“版本”。
2.2 動(dòng)態(tài)分派與多態(tài)之重寫
重寫:子類重寫父類的方法,方法名和方法簽名都相同,注意靜態(tài)方法可以重載,但是對(duì)靜態(tài)方法進(jìn)行重寫是無效的(通過子類的實(shí)例對(duì)象調(diào)用,則對(duì)應(yīng)的是父類定義的靜態(tài)方法,通過子類的類對(duì)象調(diào)用則調(diào)用子類定義的靜態(tài)方法)
public class DynamicDispatchTest {
private static void print(String str){
System.out.println(str);
}
static class Human{
protected void sayHello(){
print("human");
}
protected static void printStatic(){
print("human static");
}
}
static class Man extends Human{
@Override
protected void sayHello(){
print("man");
}
protected static void printStatic(){
print("man static");
}
}
static class Woman extends Human{
@Override
protected void sayHello(){
print("woman");
}
protected static void printStatic(){
print("woman static");
}
}
public static void main(String[] args){
//動(dòng)態(tài)分派測試
print("動(dòng)態(tài)分派測試");
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
//靜態(tài)方法重寫測試
print("靜態(tài)方法重寫測試");
Human.printStatic();
Man.printStatic();
man.printStatic();
woman.printStatic();
}
}
/*
動(dòng)態(tài)分派測試
man
woman
woman
靜態(tài)方法重寫測試
human static
man static
human static
human static
*/
解析和分派這兩者之間不是二選一的排他關(guān)系,而是不同階段的不同層次上去篩選、確定目標(biāo)方法的過程。比如,靜態(tài)方法會(huì)在類加載期間就進(jìn)行解析,而靜態(tài)方法顯然也是可以擁有重載版本的,選擇重載版本的過程也是通過靜態(tài)分派完成的。
動(dòng)態(tài)分派和多態(tài)重寫的本質(zhì)要從字節(jié)碼指令invokevirtual的多態(tài)查找過程開始說起: 1) 找到棧頂元素所指向的對(duì)象的實(shí)際類型,記為C;2) 在類型C中找到與常量池中的描述符與簡單名稱都相符的方法,然后進(jìn)行訪問權(quán)限檢查,如果通過則返回這個(gè)方法的直接引用,查找結(jié)束;如果不通過,則返回java.lang.IllegalAccessError異常。3) 否則,按照繼承關(guān)系,繼續(xù)重復(fù)2中搜索和驗(yàn)證過程。 4) 如果始終沒有找到,則拋出java.lang.AbstractMethodError異常。
調(diào)用方法時(shí),invokevirtaul指令把常量池中的類方法符號(hào)引用解析到了不同的實(shí)際類型的直接引用上,這個(gè)就是java方法中重寫的本質(zhì)。
動(dòng)態(tài)分配的實(shí)現(xiàn):動(dòng)態(tài)分派時(shí),在類的方法的元數(shù)據(jù)中搜索合適的目標(biāo)方法,基于性能的考慮,避免頻繁的搜索,會(huì)為類在方法區(qū)中建立一個(gè)虛方法表,使用虛方法表索引來代替元數(shù)據(jù)查找以提高性能。
虛方法表:虛方法表中存放著各個(gè)方法的實(shí)際入口地址。如果某個(gè)方法在子類中沒有重寫,那子類的虛方法表里面的地址入口和父類相同方法的地址入口時(shí)一致的,都指向父類的實(shí)現(xiàn)入口,如果子類中重寫了這個(gè)方法,子類方發(fā)表的地址將會(huì)替代為指向子類實(shí)現(xiàn)版本的入口地址。具有相同方法簽名的父類、子類的方法在父類和子類的虛方法表中具有相同的索引序號(hào),這樣當(dāng)類型變換時(shí),僅需要變更查找的方法表。
解釋執(zhí)行和直接執(zhí)行
- 解釋器
javac將java文件編譯成class文件,將源代碼編譯成字節(jié)碼(中間代碼),這個(gè)字節(jié)碼是與平臺(tái)無關(guān)的,而解釋器就是將字節(jié)碼翻譯成對(duì)應(yīng)平臺(tái)的機(jī)器碼,解釋執(zhí)行。
- JIT即時(shí)編譯器
分析Java應(yīng)用程序的函數(shù)調(diào)用,將熱點(diǎn)代碼將字節(jié)碼編譯為本地更高效的機(jī)器碼,JVM對(duì)這個(gè)函數(shù)就不在進(jìn)行解釋執(zhí)行了,而是直接執(zhí)行。
- 解釋執(zhí)行還是直接執(zhí)行
- 在client模式下,是解釋執(zhí)行的。
- 在server模式下:先解釋執(zhí)行,然后JVM統(tǒng)計(jì)函數(shù)執(zhí)行熱點(diǎn),將這些熱點(diǎn)代碼仔細(xì)優(yōu)化編譯成本地機(jī)器碼(默認(rèn)為調(diào)用10000次以上),然后執(zhí)行本地機(jī)器碼,當(dāng)這個(gè)熱點(diǎn)不再是熱點(diǎn)的時(shí)候,釋放編譯的代碼,重新解釋執(zhí)行。這也就是Sun JDK被稱為 HotSpot(熱點(diǎn)) VM的原因