深入理解Java之java虛擬機(jī)干凈利落的規(guī)范總結(jié) 下
- 作者:新網(wǎng)
- 來(lái)源:新網(wǎng)
- 瀏覽:100
- 2018-05-03 17:56:15
要去正確地實(shí)現(xiàn)一臺(tái)Java虛擬機(jī),就需要正確地讀取class文件中每一條字節(jié)碼指令并且能正確執(zhí)行這些指令所蘊(yùn)含的操作即可。
<
div> 要去正確地實(shí)現(xiàn)一臺(tái)Java
虛擬機(jī),就需要正確地讀取class文件中每一條字節(jié)碼指令并且能正確執(zhí)行這些指令所蘊(yùn)含的操作即可。
由Java虛擬機(jī)執(zhí)行的每個(gè)方法都會(huì)配有零到多個(gè)異常處理器。異常處理器描述了其在方法代碼中的有效作用范圍(通過(guò)字節(jié)碼偏移量范圍來(lái)描述)、能處理的異常類(lèi)型以及處理異常的代碼所在的位置。要判斷某個(gè)異常處理器是否可以處理某個(gè)具體的異常,需要同時(shí)檢查一場(chǎng)出現(xiàn)的位置是否在異常處理的有效作用范圍內(nèi),以及出現(xiàn)的異常是否是異常處理器聲明可以處理的異常類(lèi)型或其子類(lèi)型。當(dāng)拋出異常時(shí),Java虛擬機(jī)搜索當(dāng)前方法包含的各個(gè)異常處理器,如果能找到可以處理該異常的異常處理器,則將代碼控制權(quán)轉(zhuǎn)向異常處理器中描述的處理異常的分支之中。
首先簡(jiǎn)單介紹一下main方法中各條字節(jié)碼指令所代表的意思:
0 : 將int類(lèi)型的常量i壓入操作數(shù)棧中,iconst_0后面跟的那個(gè)0代表常量的值為0;
1:將一個(gè)int類(lèi)型數(shù)據(jù)由保存到本地變量表,istort_1后面的1代表的是指向當(dāng)前棧幀中局部變量表的索引值;
2:將一個(gè)值為3的int類(lèi)型的常量壓入操作數(shù)棧中。在這里指的是被除數(shù)3;
3:從局部變量表加載一個(gè)int類(lèi)型值到操作數(shù)棧中,這里指的是除數(shù)i;
4:對(duì)兩個(gè)int類(lèi)型的數(shù)據(jù)做除法;
5:將兩數(shù)相除之后所得到的int類(lèi)型數(shù)據(jù)保存到本地變量表;
6:假如沒(méi)有發(fā)生異常的話,那么執(zhí)行完goto到第14條語(yǔ)句,函數(shù)正常返回;
9:假如發(fā)生了除零異常,就執(zhí)行這條指令,將異常對(duì)象保存到局部變量表中;
10:從局部變量表中加載剛才的那個(gè)異常對(duì)象到操作數(shù)中;
11:調(diào)用異常對(duì)象的printStackTrace方法
14:不管是正常完成還是異常完成,最終都會(huì)返回。
在字節(jié)碼下方可以看到一個(gè)Exception table。那么它是什么東西呢?其實(shí)我們很容易能夠理解它就是異常表,也就是前面我們提過(guò)的異常處理器。我們可以明顯地觀察出,其實(shí)try-catch代碼塊編譯之后似乎沒(méi)有生成任何指令。那么Java語(yǔ)言中的try-catch放到字節(jié)碼當(dāng)中對(duì)應(yīng)什么東西呢?其實(shí)就是對(duì)應(yīng)這個(gè)異常處理器。下面我們來(lái)解讀一下異常處理器:
在try語(yǔ)句塊的執(zhí)行過(guò)程中如果沒(méi)有拋出異常,那么這個(gè)異常處理器不會(huì)起作用。異常處理器的作用范圍是從字節(jié)碼的第2行到第6行,也就是from-to標(biāo)明的范圍。假如編譯好的代碼里面第2~6句之間有一個(gè)類(lèi)型為
java.lang.ArithmeticException的異常實(shí)例被拋出,那么操作將轉(zhuǎn)移至第9句繼續(xù)執(zhí)行,即進(jìn)入catch語(yǔ)句塊的實(shí)踐步驟。假如說(shuō)拋出的異常不是ArithmeticException實(shí)例,那么異常處理器就不能處理該異常,這個(gè)異常將返回給上一級(jí)的調(diào)用者。
那假如try語(yǔ)句塊包含多個(gè)catch語(yǔ)句塊,在編譯好的代碼中會(huì)出現(xiàn)什么樣的結(jié)果呢?
如果給定的try語(yǔ)句塊包含多個(gè)catch語(yǔ)句塊,那么在編譯好的代碼中,多個(gè)catch語(yǔ)句塊的內(nèi)容將會(huì)連續(xù)排列,在異常表中也會(huì)有對(duì)應(yīng)的連續(xù)排列的成員,它們的排列順序和源碼中catch語(yǔ)句塊的出現(xiàn)順序一致。main方法在執(zhí)行時(shí),如果try語(yǔ)句塊中拋出了一個(gè)異常,這個(gè)異常將會(huì)被多個(gè)catch語(yǔ)句塊捕獲。假如第一個(gè)catch不能捕獲異常(當(dāng)然這里的第一個(gè)catch語(yǔ)句塊肯定是能處理ArithmeticException,我只是舉個(gè)例子),那么異常將交由第二個(gè)異常處理器來(lái)進(jìn)行處理,這很容易理解。因?yàn)槲以诘诙€(gè)catch語(yǔ)句塊中選擇的是將捕獲的異常拋出,所以在字節(jié)碼的第26行可以看到有一個(gè)athrow指令,在前面的學(xué)習(xí)當(dāng)中我們知道它是拋出異常的意思,其實(shí)也就是對(duì)應(yīng)著Java代碼中的throw new Exception()。在這里,我還要順便介紹一下Java創(chuàng)建一個(gè)對(duì)象的代碼在編譯之后會(huì)產(chǎn)生怎樣的字節(jié)碼。
其實(shí),剛才我所說(shuō)的throw new Exception()對(duì)應(yīng)athrow字節(jié)碼指令只說(shuō)對(duì)了一半,它在編譯之后不僅僅只產(chǎn)生athrow這一條字節(jié)碼指令。因?yàn)樗€對(duì)應(yīng)著一個(gè)操作,也就是new一個(gè)Exception對(duì)象。Java語(yǔ)言實(shí)例化一個(gè)Exception對(duì)象將會(huì)產(chǎn)生三條字節(jié)碼指令,即上圖中19,22,23三行:
為什么會(huì)有三條指令呢?dup是做什么的?我們下面一起來(lái)學(xué)習(xí)一下
由于討論的是創(chuàng)建對(duì)象,所以在代碼throw new Exception()中我們不看throw,只看new Exception()這一部分代碼。
new Exception()表達(dá)式的作用是:
創(chuàng)建并默認(rèn)初始化一個(gè)Exception對(duì)象;
調(diào)用Exceptioon類(lèi)的signature為()V的構(gòu)造器;
表達(dá)式的值為一個(gè)指向這個(gè)對(duì)象的引用
對(duì)應(yīng)字節(jié)碼,我們可以看到:
new Exception()對(duì)應(yīng)上面的1
invokespecial Exception.()V對(duì)應(yīng)上面的2
那么3是怎么來(lái)的?
回歸到字節(jié)碼,我們可以看到new字節(jié)碼指令的作用是創(chuàng)建指定類(lèi)型的對(duì)象實(shí)例、對(duì)其進(jìn)行默認(rèn)初始化,并將指向該實(shí)例的一個(gè)引用壓入操作數(shù)棧頂;
然后因?yàn)閕nvokespecial會(huì)消耗操作數(shù)棧頂?shù)囊米鳛閭鹘o構(gòu)造器的"this"參數(shù),所以如果我們希望在invokespecial調(diào)用后在操作數(shù)棧頂還維持有一個(gè)指向新建對(duì)象的引用,就得在invokespecial之前先“復(fù)制”一份引用----這就是dup的來(lái)源。
以上,就是對(duì)創(chuàng)建一個(gè)對(duì)象編譯之后產(chǎn)生的字節(jié)碼的解釋
編譯finally語(yǔ)句塊
剛才我們介紹了異常處理在字節(jié)碼層面的細(xì)節(jié),但是我們還需要注意的是----由于finally能夠保證不管發(fā)生任何情況,都能夠執(zhí)行語(yǔ)句塊中的代碼,所以在日常編碼過(guò)程中我們?cè)诳赡馨l(fā)生異常的地方(或者是不會(huì)發(fā)生異常的地方)經(jīng)常使用finally來(lái)釋放某些資源。
下面我們從虛擬機(jī)層面來(lái)看看如何保證finally語(yǔ)句塊中的代碼一定會(huì)執(zhí)行
可以看到,其實(shí)編譯器是通過(guò)在每個(gè)分支后面增加冗余代碼的形式來(lái)保證finally語(yǔ)句塊中的代碼一定會(huì)被執(zhí)行。這里和書(shū)上講的有點(diǎn)出入,書(shū)上在講解這一塊的時(shí)候還是用jsr、jsr_w、ret等程序控制轉(zhuǎn)移指令來(lái)解釋的,但是javac在很早之前就不再為finally語(yǔ)句生成jsr和ret指令了。
如果程序在try語(yǔ)句塊中執(zhí)行了return,那么代碼的行為如下:
如果有返回值,將返回值保存在局部變量表;
執(zhí)行跟在后面的冗余finally語(yǔ)句塊中的代碼;
在finally執(zhí)行完之后,將事先保存在局部變量表中的返回值壓入操作數(shù)棧中之后返回。
如果在try語(yǔ)句中拋出異常,那么代碼的行為如下:
將異常保存在局部變量表中
執(zhí)行finally語(yǔ)句塊中的代碼
在執(zhí)行完finally語(yǔ)句塊中的代碼后,重新拋出這個(gè)事先保存好的異常。
Java虛擬機(jī)中的同步(synchronization)使用monitor的進(jìn)入和退出來(lái)實(shí)現(xiàn)的。無(wú)論顯式同步(有明確的monitorenter和monitorexit指令),還是隱式同步(依賴方法調(diào)用和返回指令實(shí)現(xiàn))都是如此。
在Java語(yǔ)言中,同步用得最多的地方可能是經(jīng)synchronized所修飾的同步方法。同步方法并不是用monitorenter和monitorexit來(lái)實(shí)現(xiàn)的,而是由方法調(diào)用指令讀取運(yùn)行時(shí)常量池中方法的ACC_SYNCHRONIZED標(biāo)志來(lái)隱式實(shí)現(xiàn)的。
monitorenter和monitorexit指令用于編譯同步語(yǔ)句塊
編譯器必須確保無(wú)論方法以何種方式完成(正常結(jié)束或者是異常結(jié)束),方法中調(diào)用過(guò)的每條monitorenter指令都必須有對(duì)應(yīng)的monitorexit指令得到執(zhí)行。為了確保在方法異常完成時(shí),monitorenter和monitorexit指令依然可以正確配對(duì)執(zhí)行,編譯器會(huì)自動(dòng)生成一個(gè)異常處理器,這個(gè)異常處理器宣稱自己可以處理所有異常,它的代碼用來(lái)執(zhí)行monitorexit指令。
到這里深入理解Java之java虛擬機(jī)干凈利落的規(guī)范總結(jié)就結(jié)束了,不足之處還望大家多多包涵!!覺(jué)得收獲的話可以點(diǎn)個(gè)關(guān)注收藏轉(zhuǎn)發(fā)一波喔,謝謝大佬們支持。