我們都知道,在當(dāng)前的Java中(1.0)之后,編譯器講源代碼轉(zhuǎn)成字節(jié)碼,那么字節(jié)碼如何被執(zhí)行的呢?這就涉及到了JVM的字節(jié)碼執(zhí)行引擎,執(zhí)行引擎負(fù)責(zé)具體的代碼調(diào)用及執(zhí)行過(guò)程。就目前而言,所有的執(zhí)行引擎的基本一致:
創(chuàng)新互聯(lián)專業(yè)網(wǎng)站建設(shè)、成都網(wǎng)站制作,集網(wǎng)站策劃、網(wǎng)站設(shè)計(jì)、網(wǎng)站制作于一體,網(wǎng)站seo、網(wǎng)站優(yōu)化、網(wǎng)站營(yíng)銷、軟文發(fā)布平臺(tái)等專業(yè)人才根據(jù)搜索規(guī)律編程設(shè)計(jì),讓網(wǎng)站在運(yùn)行后,在搜索中有好的表現(xiàn),專業(yè)設(shè)計(jì)制作為您帶來(lái)效益的網(wǎng)站!讓網(wǎng)站建設(shè)為您創(chuàng)造效益。輸入:字節(jié)碼文件
處理:字節(jié)碼解析
輸出:執(zhí)行結(jié)果。
物理機(jī)的執(zhí)行引擎是由硬件實(shí)現(xiàn)的,和物理機(jī)的執(zhí)行過(guò)程不同的是虛擬機(jī)的執(zhí)行引擎由于自己實(shí)現(xiàn)的。
每一個(gè)線程都有一個(gè)棧,也就是前文中提到的虛擬機(jī)棧,棧中的基本元素我們稱之為棧幀。棧幀是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。每個(gè)棧幀都包括了一下幾部分:局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法的返回地址 和一些額外的附加信息。棧幀中需要多大的局部變量表和多深的操作數(shù)棧在編譯代碼的過(guò)程中已經(jīng)完全確定,并寫入到方法表的Code屬性中。在活動(dòng)的線程中,位于當(dāng)前棧頂?shù)臈攀怯行У模Q之為當(dāng)前幀,與這個(gè)棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法。執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令只針對(duì)當(dāng)前棧幀進(jìn)行操作。需要注意的是一個(gè)棧中能容納的棧幀是受限,過(guò)深的方法調(diào)用可能會(huì)導(dǎo)致StackOverFlowError,當(dāng)然,我們可以認(rèn)為設(shè)置棧的大小。其模型示意圖大體如下:
針對(duì)上面的棧結(jié)構(gòu),我們重點(diǎn)解釋一下局部變量表,操作棧,指令計(jì)數(shù)器幾個(gè)概念:
是變量值的存儲(chǔ)空間,由方法參數(shù)和方法內(nèi)部定義的局部變量組成,其容量用Slot1作為最小單位。在編譯期間,就在方法的Code屬性的max_locals數(shù)據(jù)項(xiàng)中確定了該方法所需要分配的局部變量表的大容量。由于局部變量表是建立在線程的棧上,是線程的私有數(shù)據(jù),因此不存在數(shù)據(jù)安全問(wèn)題。在方法執(zhí)行時(shí),虛擬機(jī)通過(guò)使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過(guò)程。如果是實(shí)例方法,那局部變量表第0位索引的Slot存儲(chǔ)的是方法所屬對(duì)象實(shí)例的引用,因此在方法內(nèi)可以通過(guò)關(guān)鍵字this來(lái)訪問(wèn)到這個(gè)隱含的參數(shù)。其余的參數(shù)按照參數(shù)表順序排列,參數(shù)表分配完畢之后,再根據(jù)方法體內(nèi)定義的變量的順序和作用域分配。我們知道類變量表有兩次初始化的機(jī)會(huì),第一次是在“準(zhǔn)備階段”,執(zhí)行系統(tǒng)初始化,對(duì)類變量設(shè)置零值,另一次則是在“初始化”階段,賦予程序員在代碼中定義的初始值。和類變量初始化不同的是,局部變量表不存在系統(tǒng)初始化的過(guò)程,這意味著一旦定義了局部變量則必須人為的初始化,否則無(wú)法使用。舉例說(shuō)明:
為了方便起見(jiàn),假設(shè)以上兩段代碼在同一個(gè)類中。這時(shí)call()所對(duì)應(yīng)的棧幀中的局部變量表大體如下:
而call2()所對(duì)應(yīng)的棧幀的局部變量表大體如下:
后入先出棧,由字節(jié)碼指令往棧中存數(shù)據(jù)和取數(shù)據(jù),棧中的任何一個(gè)元素都是可以任意的Java數(shù)據(jù)類型。和局部變量類似,操作數(shù)棧的大深度也在編譯的時(shí)候?qū)懭氲紺ode屬性的max_stacks數(shù)據(jù)項(xiàng)中。當(dāng)一個(gè)方法剛開(kāi)始執(zhí)行的時(shí)候,這個(gè)方法的操作數(shù)棧是空的,在方法的執(zhí)行過(guò)程中,會(huì)有各種字節(jié)碼指令往操作數(shù)中寫入和提取內(nèi)容,也就是出棧/入棧操作。操作數(shù)棧中元素的數(shù)據(jù)類型必須與字節(jié)碼指令的序列嚴(yán)格匹配2,這由編譯器在編譯器期間進(jìn)行驗(yàn)證,同時(shí)在類加載過(guò)程中的類檢驗(yàn)階段的數(shù)據(jù)流分析階段要再次驗(yàn)證。另外我們說(shuō)Java虛擬機(jī)的解釋引擎是基于棧的執(zhí)行引擎,其中的棧指的就是操作數(shù)棧。
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,持有該引用是為了支持方法調(diào)用過(guò)程中的動(dòng)態(tài)連接。
存放調(diào)用調(diào)用該方法的pc計(jì)數(shù)器的值。當(dāng)一個(gè)方法開(kāi)始之后,只有兩種方式可以退出這個(gè)方法:1、執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,也就是所謂的正常完成出口。2、在方法執(zhí)行的過(guò)程中遇到了異常,并且這個(gè)異常沒(méi)有在方法內(nèi)進(jìn)行處理,也就是只要在本方法的異常表中沒(méi)有搜索到匹配的異常處理器,就會(huì)導(dǎo)致方法退出,這種方式成為異常完成出口。正常完成出口和異常完成出口的區(qū)別在于:通過(guò)異常完成出口退出的不會(huì)給他的上層調(diào)用者產(chǎn)生任何的返回值。
無(wú)論通過(guò)哪種方式退出,在方法退出后都返回到該方法被調(diào)用的位置,方法正常退出時(shí),調(diào)用者的pc計(jì)數(shù)器的值作為返回地址,而通過(guò)異常退出的,返回地址是要通過(guò)異常處理器表來(lái)確定,棧幀中一般不會(huì)保存這部分信息。本質(zhì)上,方法的退出就是當(dāng)前棧幀出棧的過(guò)程。
方法調(diào)用的主要任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個(gè)方法),該過(guò)程不涉及方法具體的運(yùn)行過(guò)程。按照調(diào)用方式共分為兩類:
解析調(diào)用是靜態(tài)的過(guò)程,在編譯期間就完全確定目標(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),后者在外部不可訪問(wèn),因此決定了他們都不可能通過(guò)繼承或者別的方式重寫該方法,符合這兩類的方法主要有以下幾種:靜態(tài)方法、私有方法、實(shí)例構(gòu)造器、父類方法。虛擬機(jī)中提供了以下幾條方法調(diào)用指令:
invokestatic:調(diào)用靜態(tài)方法,解析階段確定唯一方法版本
invokespecial:調(diào)用
方法、私有及父類方法,解析階段確定唯一方法版本
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修飾的除外[^footnote4])稱為虛方法。
分派調(diào)用更多的體現(xiàn)在多態(tài)上。
靜態(tài)分派:所有依賴靜態(tài)類型3來(lái)定位方法執(zhí)行版本的分派成為靜態(tài)分派,發(fā)生在編譯階段,典型應(yīng)用是方法 重載 。
動(dòng)態(tài)分派:在運(yùn)行期間根據(jù)實(shí)際類型4來(lái)確定方法執(zhí)行版本的分派成為動(dòng)態(tài)分派,發(fā)生在程序運(yùn)行期間,典型的應(yīng)用是方法的 重寫 。
單分派:根據(jù)一個(gè)宗量5 對(duì)目標(biāo)方法進(jìn)行選擇。
多分派:根據(jù)多于一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇。
動(dòng)態(tài)分派在Java中被大量使用,使用頻率及其高,如果在每次動(dòng)態(tài)分派的過(guò)程中都要重新在類的方法元數(shù)據(jù)中搜索合適的目標(biāo)的話就可能影響到執(zhí)行效率,因此JVM在類的方法區(qū)中建立虛方法表(virtual method table)來(lái)提高性能。每個(gè)類中都有一個(gè)虛方法表,表中存放著各個(gè)方法的實(shí)際入口。如果某個(gè)方法在子類中沒(méi)有被重寫,那子類的虛方法表中該方法的地址入口和父類該方法的地址入口一樣,即子類的方法入口指向父類的方法入口。如果子類重寫父類的方法,那么子類的虛方法表中該方法的實(shí)際入口將會(huì)被替換為指向子類實(shí)現(xiàn)版本的入口地址。
那么虛方法表什么時(shí)候被創(chuàng)建?虛方法表會(huì)在類加載的連接階段被創(chuàng)建并開(kāi)始初始化,類的變量初始值準(zhǔn)備完成之后,JVM會(huì)把該類的方法表也初始化完畢。
在jdk 1.0時(shí)代,Java虛擬機(jī)完全是解釋執(zhí)行的,隨著技術(shù)的發(fā)展,現(xiàn)在主流的虛擬機(jī)中大都包含了即時(shí)編譯器(JIT)。因此,虛擬機(jī)在執(zhí)行代碼過(guò)程中,到底是解釋執(zhí)行還是編譯執(zhí)行,只有它自己才能準(zhǔn)確判斷了,但是無(wú)論什么虛擬機(jī),其原理基本符合現(xiàn)代經(jīng)典的編譯原理,如下圖所示:
在Java中,javac編譯器完成了詞法分析、語(yǔ)法分析以及抽象語(yǔ)法樹(shù)的過(guò)程,最終遍歷語(yǔ)法樹(shù)生成線性字節(jié)碼指令流的過(guò)程,此過(guò)程發(fā)生在虛擬機(jī)外部。
Java編譯器輸入的指令流基本上是一種基于
棧
的指令集架構(gòu),指令流中的指令大部分是零地址指令,其執(zhí)行過(guò)程依賴于操作棧。另外一種指令集架構(gòu)則是基于
寄存器
的指令集架構(gòu),典型的應(yīng)用是x86的二進(jìn)制指令集,比如傳統(tǒng)的PC以及Android的Davlik虛擬機(jī)。兩者之間最直接的區(qū)別是,基于棧的指令集架構(gòu)不需要硬件的支持,而基于寄存器的指令集架構(gòu)則完全依賴硬件,這意味基于寄存器的指令集架構(gòu)執(zhí)行效率更高,單可移植性差,而基于棧的指令集架構(gòu)的移植性更高,但執(zhí)行效率相對(duì)較慢,初次之外,相同的操作,基于棧的指令集往往需要更多的指令,比如同樣執(zhí)行2+3這種邏輯操作,其指令分別如下:
基于棧的計(jì)算流程(以Java虛擬機(jī)為例):
而基于寄存器的計(jì)算流程:
下面我們用簡(jiǎn)單的案例來(lái)解釋一下JVM代碼執(zhí)行的過(guò)程,代碼實(shí)例如下:
使用javap指令查看字節(jié)碼:
執(zhí)行過(guò)程中代碼、操作數(shù)棧和局部變量表的變化情況如下:
也成為容量槽,虛擬規(guī)范中并沒(méi)有規(guī)定一個(gè)Slot應(yīng)該占據(jù)多大的內(nèi)存空間。
這里的嚴(yán)格匹配指的是字節(jié)碼操作的棧中的實(shí)際元素類型必須要字節(jié)碼規(guī)定的元素類型一致。比如iadd指令規(guī)定操作兩個(gè)整形數(shù)據(jù),那么在操作棧中的實(shí)際元素的時(shí)候,棧中的兩個(gè)元素也必須是整形。
Animal dog=new Dog();其中的Animal我們稱之為靜態(tài)類型,而Dog稱之為動(dòng)態(tài)類型。兩者都可以發(fā)生變化,區(qū)別在于靜態(tài)類型只在使用時(shí)發(fā)生變化,變量本身的靜態(tài)類型不會(huì)被改變,最終的靜態(tài)類型是在編譯期間可知的,而實(shí)際類型則是在運(yùn)行期才可確定。
Animal dog=new Dog();其中的Animal我們稱之為靜態(tài)類型,而Dog稱之為動(dòng)態(tài)類型。兩者都可以發(fā)生變化,區(qū)別在于靜態(tài)類型只在使用時(shí)發(fā)生變化,變量本身的靜態(tài)類型不會(huì)被改變,最終的靜態(tài)類型是在編譯期間可知的,而實(shí)際類型則是在運(yùn)行期才可確定。
宗量:方法的接受者與方法的參數(shù)稱為方法的宗量。
舉個(gè)例子:
public void dispatcher(){
int result=this.execute(8,9);
}
public void execute(int pointX,pointY){
//TODO
}
在dispatcher()方法中調(diào)用了execute(8,9),那此時(shí)的方法接受者為當(dāng)前this指向的對(duì)象,8、9為方法的參數(shù),this對(duì)象和參數(shù)就是我們所說(shuō)的宗量。
【本文轉(zhuǎn)載自Java高級(jí)架構(gòu)師,原文鏈接:https://mp.weixin.qq.com/s/rXdd7zEJxY4SBSSAg5Dw3w,轉(zhuǎn)載授權(quán)請(qǐng)聯(lián)系原作者】