本文轉(zhuǎn)自:https://www.cnblogs.com/snailclimb/p/9086337.html
本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內(nèi)容請到我的倉庫里查看
喜歡的話麻煩點下Star哈
文章將同步到我的個人博客:
本文是微信公眾號【Java技術(shù)江湖】的《深入理解JVM虛擬機》其中一篇宣赔,本文部分內(nèi)容來源于網(wǎng)絡(luò)预麸,為了把本文主題講得清晰透徹,也整合了很多我認(rèn)為不錯的技術(shù)博客內(nèi)容儒将,引用其中了一些比較好的博客文章,如有侵權(quán)钩蚊,請聯(lián)系作者蹈矮。
該系列博文會告訴你如何從入門到進(jìn)階鸣驱,一步步地學(xué)習(xí)JVM基礎(chǔ)知識,并上手進(jìn)行JVM調(diào)優(yōu)實戰(zhàn)踊东,JVM是每一個Java工程師必須要學(xué)習(xí)和理解的知識點,你必須要掌握其實現(xiàn)原理闸翅,才能更完整地了解整個Java技術(shù)體系再芋,形成自己的知識框架。
為了更好地總結(jié)和檢驗?zāi)愕膶W(xué)習(xí)成果坚冀,本系列文章也會提供每個知識點對應(yīng)的面試題以及參考答案祝闻。
如果對本系列文章有什么建議,或者是有什么疑問的話遗菠,也可以關(guān)注公眾號【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂华蜒。
1 概述
執(zhí)行引擎是java虛擬機最核心的組成部件之一辙纬。虛擬機的執(zhí)行引擎由自己實現(xiàn),所以可以自行定制指令集與執(zhí)行引擎的結(jié)構(gòu)體系叭喜,并且能夠執(zhí)行那些不被硬件直接支持的指令集格式贺拣。
所有的Java虛擬機的執(zhí)行引擎都是一致的:輸入的是字節(jié)碼文件,處理過程是字節(jié)碼解析的等效過程捂蕴,輸出的是執(zhí)行結(jié)果譬涡。本節(jié)將主要從概念模型的角度來講解虛擬機的方法調(diào)用和字節(jié)碼執(zhí)行。
2 運行時棧幀結(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)著一個棧幀在虛擬機棧里面從入棧到出棧的過程级乍。
棧幀概念結(jié)構(gòu)如下圖所示:
2.1 局部變量表
局部變量表是一組變量值存儲空間舌劳,用于存放方法參數(shù)和方法內(nèi)定義的局部變量。
局部變量表的容量以變量槽(Variable Slot)為最小單位玫荣。 一個Slot可以存放一個32位以內(nèi)(boolean甚淡、byte、char捅厂、short贯卦、int资柔、float、reference和returnAddress)的數(shù)據(jù)類型脸侥,reference類型表示一個對象實例的引用睁枕,returnAddress已經(jīng)很少見了外遇,可以忽略。
對于64位的數(shù)據(jù)類型(Java語言中明確的64位數(shù)據(jù)類型只有l(wèi)ong和double)诡渴,虛擬機會以高位對齊的方式為其分配兩個連續(xù)的Slot空間妄辩。
虛擬機通過索引定位的方式使用局部變量表眼耀,索引值的范圍從0開始至局部變量表最大的Slot數(shù)量哮伟。訪問的是32位數(shù)據(jù)類型的變量楞黄,索引n就代表了使用第n個Slot,如果是64位數(shù)據(jù)類型鬼廓,就代表會同時使用n和n+1這兩個Slot桑阶。
為了節(jié)省棧幀空間蚣录,局部變量Slot可以重用萎河,方法體中定義的變量虐杯,其作用域并不一定會覆蓋整個方法體。如果當(dāng)前字節(jié)碼PC計數(shù)器的值超出了某個變量的作用域支子,那么這個變量的Slot就可以交給其他變量使用值朋。這樣的設(shè)計會帶來一些額外的副作用昨登,比如:在某些情況下丰辣,Slot的復(fù)用會直接影響到系統(tǒng)的收集行為禽捆。
2.2 操作數(shù)棧
操作數(shù)棧(Operand Stack) 也常稱為操作棧胚想,它是一個后入先出棧顿仇。當(dāng)一個方法執(zhí)行開始時臼闻,這個方法的操作數(shù)棧是空的述呐,在方法執(zhí)行過程中乓搬,會有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容进肯,也就是 出棧/入棧操作江掩。
在概念模型中策泣,一個活動線程中兩個棧幀是相互獨立的抬吟。但大多數(shù)虛擬機實現(xiàn)都會做一些優(yōu)化處理:讓下一個棧幀的部分操作數(shù)棧與上一個棧幀的部分局部變量表重疊在一起火本,這樣的好處是方法調(diào)用時可以共享一部分?jǐn)?shù)據(jù)发侵,而無須進(jìn)行額外的參數(shù)復(fù)制傳遞刃鳄。
2.3 動態(tài)連接
每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用叔锐,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接愉烙;
字節(jié)碼中方法調(diào)用指令是以常量池中的指向方法的符號引用為參數(shù)的步责,有一部分符號引用會在類加載階段或第一次使用的時候轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為 靜態(tài)解析遂鹊,另外一部分在每次的運行期間轉(zhuǎn)化為直接引用秉扑,這部分稱為動態(tài)連接舟陆。
2.4 方法返回地址
當(dāng)一個方法被執(zhí)行后秦躯,有兩種方式退出這個方法:
第一種是執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令宦赠,這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)。
另外一種是在方法執(zhí)行過程中遇到了異常毡琉,并且這個異常沒有在方法體內(nèi)得到處理(即本方法異常處理表中沒有匹配的異常處理器)桅滋,就會導(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)用指令后面的一條指令等午磁。
2.5 附加信息
虛擬機規(guī)范允許虛擬機實現(xiàn)向棧幀中添加一些自定義的附加信息迅皇,例如與調(diào)試相關(guān)的信息等登颓。
3 方法調(diào)用
方法調(diào)用階段的目的:確定被調(diào)用方法的版本(哪一個方法)框咙,不涉及方法內(nèi)部的具體運行過程喇嘱,在程序運行時者铜,進(jìn)行方法調(diào)用是最普遍放椰、最頻繁的操作砾医。
一切方法調(diào)用在Class文件里存儲的都只是符號引用藻烤,這是需要在類加載期間或者是運行期間怖亭,才能確定為方法在實際 運行時內(nèi)存布局中的入口地址(相當(dāng)于之前說的直接引用)兴猩。
3.1 解析
“編譯期可知倾芝,運行期不可變”的方法(靜態(tài)方法和私有方法)晨另,在類加載的解析階段谱姓,會將其符號引用轉(zhuǎn)化為直接引用(入口地址)屉来。這類方法的調(diào)用稱為“解析(Resolution)”。
在Java虛擬機中提供了5條方法調(diào)用字節(jié)碼指令:
- invokestatic : 調(diào)用靜態(tài)方法
- invokespecial:調(diào)用實例構(gòu)造器方法茂契、私有方法掉冶、父類方法
- invokevirtual:調(diào)用所有的虛方法
- invokeinterface:調(diào)用接口方法厌小,會在運行時在確定一個實現(xiàn)此接口的對象
- invokedynamic:先在運行時動態(tài)解析出點限定符所引用的方法,然后再執(zhí)行該方法旁振,在此之前的4條調(diào)用命令的分派邏輯是固化在Java虛擬機內(nèi)部的,而invokedynamic指令的分派邏輯是由用戶所設(shè)定的引導(dǎo)方法決定的梢薪。
3.2 分派
分派調(diào)用過程將會揭示多態(tài)性特征的一些最基本的體現(xiàn)尝哆,如“重載”和“重寫”在Java虛擬中是如何實現(xiàn)的秋泄。
1 靜態(tài)分派
所有依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動作恒序,都稱為靜態(tài)分派歧胁。靜態(tài)分派發(fā)生在編譯階段。
靜態(tài)分派最典型的應(yīng)用就是方法重載屠缭。
package jvm8_3_2;
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("Human guy");
}
public void sayhello(Man guy) {
System.out.println("Man guy");
}
public void sayhello(Woman guy) {
System.out.println("Woman guy");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch staticDispatch = new StaticDispatch();
staticDispatch.sayhello(man);// Human guy
staticDispatch.sayhello(woman);// Human guy
}
}
運行結(jié)果:
Human guy
Human guy
為什么會出現(xiàn)這樣的結(jié)果呢?
Human man = new Man();其中的Human稱為變量的靜態(tài)類型(Static Type),Man稱為變量的實際類型(Actual Type)阵翎。
兩者的區(qū)別是:靜態(tài)類型在編譯器可知郭卫,而實際類型到運行期才確定下來贰军。
在重載時通過參數(shù)的靜態(tài)類型而不是實際類型作為判定依據(jù)词疼,因此,在編譯階段许饿,Javac編譯器會根據(jù)參數(shù)的靜態(tài)類型決定使用哪個重載版本陋率。所以選擇了sayhello(Human)作為調(diào)用目標(biāo),并把這個方法的符號引用寫到main()方法里的兩條invokevirtual指令的參數(shù)中筒愚。
2 動態(tài)分派
在運行期根據(jù)實際類型確定方法執(zhí)行版本的分派過程稱為動態(tài)分派巢掺。最典型的應(yīng)用就是方法重寫陆淀。
package jvm8_3_2;
public class DynamicDisptch {
static abstract class Human {
abstract void sayhello();
}
static class Man extends Human {
@Override
void sayhello() {
System.out.println("man");
}
}
static class Woman extends Human {
@Override
void sayhello() {
System.out.println("woman");
}
}
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
woman
woman
3 單分派和多分派
方法的接收者倔约、方法的參數(shù)都可以稱為方法的宗量坝初。根據(jù)分批基于多少種宗量绢要,可以將分派劃分為單分派和多分派重罪。單分派是根據(jù)一個宗量對目標(biāo)方法進(jìn)行選擇的哀九,多分派是根據(jù)多于一個的宗量對目標(biāo)方法進(jìn)行選擇的阅束。
Java在進(jìn)行靜態(tài)分派時息裸,選擇目標(biāo)方法要依據(jù)兩點:一是變量的靜態(tài)類型是哪個類型呼盆,二是方法參數(shù)是什么類型访圃。因為要根據(jù)兩個宗量進(jìn)行選擇,所以Java語言的靜態(tài)分派屬于多分派類型平绩。
運行時階段的動態(tài)分派過程,由于編譯器已經(jīng)確定了目標(biāo)方法的簽名(包括方法參數(shù))笆搓,運行時虛擬機只需要確定方法的接收者的實際類型满败,就可以分派算墨。因為是根據(jù)一個宗量作為選擇依據(jù)净嘀,所以Java語言的動態(tài)分派屬于單分派類型挖藏。
注:到JDK1.7時厢漩,Java語言還是靜態(tài)多分派溜嗜、動態(tài)單分派的語言,未來有可能支持動態(tài)多分派辟躏。
4 虛擬機動態(tài)分派的實現(xiàn)
由于動態(tài)分派是非常頻繁的動作,而動態(tài)分派在方法版本選擇過程中又需要在方法元數(shù)據(jù)中搜索合適的目標(biāo)方法野哭,虛擬機實現(xiàn)出于性能的考慮拨黔,通常不直接進(jìn)行如此頻繁的搜索篱蝇,而是采用優(yōu)化方法零截。
其中一種“穩(wěn)定優(yōu)化”手段是:在類的方法區(qū)中建立一個虛方法表(Virtual Method Table, 也稱vtable, 與此對應(yīng),也存在接口方法表——Interface Method Table涧衙,也稱itable)哪工。使用虛方法表索引來代替元數(shù)據(jù)查找以提高性能。其原理與C++的虛函數(shù)表類似弧哎。
虛方法表中存放的是各個方法的實際入口地址雁比。如果某個方法在子類中沒有被重寫,那子類的虛方法表里面的地址入口和父類中該方法相同撤嫩,都指向父類的實現(xiàn)入口偎捎。虛方法表一般在類加載的連接階段進(jìn)行初始化。
3.3 動態(tài)類型語言的支持
JDK新增加了invokedynamic指令來是實現(xiàn)“動態(tài)類型語言”序攘。
靜態(tài)語言和動態(tài)語言的區(qū)別:
-
靜態(tài)語言(強類型語言):
靜態(tài)語言是在編譯時變量的數(shù)據(jù)類型即可確定的語言茴她,多數(shù)靜態(tài)類型語言要求在使用變量之前必須聲明數(shù)據(jù)類型程奠。
例如:C++、Java、Delphi、C#等们衙。 -
動態(tài)語言(弱類型語言) :
動態(tài)語言是在運行時確定數(shù)據(jù)類型的語言忆蚀。變量使用之前不需要類型聲明,通常變量的類型是被賦值的那個值的類型察皇。
例如PHP/ASP/Ruby/Python/Perl/ABAP/SQL/JavaScript/Unix Shell等等。 -
強類型定義語言 :
強制數(shù)據(jù)類型定義的語言胰坟。也就是說吹缔,一旦一個變量被指定了某個數(shù)據(jù)類型晚碾,如果不經(jīng)過強制轉(zhuǎn)換廊移,那么它就永遠(yuǎn)是這個數(shù)據(jù)類型了。舉個例子:如果你定義了一個整型變量a,那么程序根本不可能將a當(dāng)作字符串類型處理。強類型定義語言是類型安全的語言芋类。 -
弱類型定義語言 :
數(shù)據(jù)類型可以被忽略的語言贮竟。它與強類型定義語言相反, 一個變量可以賦不同數(shù)據(jù)類型的值惰拱。強類型定義語言在速度上可能略遜色于弱類型定義語言,但是強類型定義語言帶來的嚴(yán)謹(jǐn)性能夠有效的避免許多錯誤勾怒。
4 基于棧的字節(jié)碼解釋執(zhí)行引擎
虛擬機如何調(diào)用方法的內(nèi)容已經(jīng)講解完畢卡乾,現(xiàn)在我們來探討虛擬機是如何執(zhí)行方法中的字節(jié)碼指令误堡。
4.1 解釋執(zhí)行
Java語言經(jīng)常被人們定位為 “解釋執(zhí)行”語言,在Java初生的JDK1.0時代摘完,這種定義還比較準(zhǔn)確的,但當(dāng)主流的虛擬機中都包含了即時編譯后,Class文件中的代碼到底會被解釋執(zhí)行還是編譯執(zhí)行,就成了只有虛擬機自己才能準(zhǔn)確判斷的事情募舟。再后來,Java也發(fā)展出來了直接生成本地代碼的編譯器[如何GCJ(GNU Compiler for the Java)]缨睡,而C/C++也出現(xiàn)了通過解釋器執(zhí)行的版本(如CINT),這時候再籠統(tǒng)的說“解釋執(zhí)行”,對于整個Java語言來說就成了幾乎沒有任何意義的概念幽纷,只有確定了談?wù)搶ο笫悄撤N具體的Java實現(xiàn)版本和執(zhí)行引擎運行模式時祭往,談解釋執(zhí)行還是編譯執(zhí)行才會比較確切。
Java語言中,javac編譯器完成了程序代碼經(jīng)過詞法分析揩尸、語法分析到抽象語法樹折联,再遍歷語法樹生成線性的字節(jié)碼指令流的過程抠艾,因為這一部分動作是在Java虛擬機之外進(jìn)行的,而解釋器在虛擬機內(nèi)部鱼炒,所以Java程序的編譯就是半獨立實現(xiàn)的浙宜,
4.2 基于棧的指令集和基于寄存器的指令集
Java編譯器輸出的指令流,基本上是一種基于棧的指令集架構(gòu)(Instruction Set Architecture干像,ISA)臣镣,依賴操作數(shù)棧進(jìn)行工作。與之相對應(yīng)的另一套常用的指令集架構(gòu)是基于寄存器的指令集部服, 依賴寄存器進(jìn)行工作筹裕。
那么,基于棧的指令集和基于寄存器的指令集這兩者有什么不同呢锯蛀?
舉個簡單例子,分別使用這兩種指令計算1+1的結(jié)果纷闺,基于棧的指令集會是這個樣子:
iconst_1
iconst_1
iadd
istore_0
兩條iconst_1指令連續(xù)把兩個常量1壓入棧后,iadd指令把棧頂?shù)膬蓚€值出棧奢讨、相加蟀伸,然后將結(jié)果放回棧頂娜睛,最后istore_0把棧頂?shù)闹捣诺骄植孔兞勘碇械牡?個Slot中。
如果基于寄存器的指令集元媚,那程序可能會是這個樣子:
mov eax, 1
add eax, 1
mov指令把EAX寄存器的值設(shè)置為1,然后add指令再把這個值加1嗤无,將結(jié)果就保存在EAX寄存器里面割疾。
基于棧的指令集主要的優(yōu)點就是可移植,寄存器是由硬件直接提供,程序直接依賴這些硬件寄存器則不可避免地要受到硬件的約束首有。
棧架構(gòu)的指令集還有一些其他的優(yōu)點您旁,如代碼相對更加緊湊侦副,編譯器實現(xiàn)更加簡單等。
棧架構(gòu)指令集的主要缺點是執(zhí)行速度相對來說會稍微慢一些。
總結(jié)
本節(jié)中厅目,我們分析了虛擬機在執(zhí)行代碼時,如何找到正確的方法渔欢、如何執(zhí)行方法內(nèi)的字節(jié)碼,以及執(zhí)行代碼時涉及的內(nèi)存結(jié)構(gòu)瘟忱。