字節碼指令---異常處理
每個時刻正在執行的當前方法就是虛擬機棧頂的棧幀。方法的執行就對應著棧幀在虛擬機中入棧和出棧的過程。當一個方法執行完,有兩種情況,一種是正常執行,另一種是異常。
完成出口(返回地址)
正常返回:(調用程序計數器中的返回地址)
三部曲:
- 恢復上層方法的局部變量表和操作數棧
- 把返回值(如果有的話)壓入調用者棧幀的操作數棧中。
- 調整程序計數器的值指向方法調用指令后面的一條指令。
異常返回
通過異常處理表中的<非棧幀中的>來確定
異常機制
如果熟悉JAVA語言,那么對以上異常繼承體系一定不會陌生。其中Error和RuntimeException是非檢查型異常,也就是不需要去catch或throw的異常。
異常表
在synchronized生成的字節碼中,其中包含了兩條monitorexit指令,是為了保證所有的異常條件都能夠退出。可以看到,編譯后的字節碼,都帶有一個叫Exception table的異常表,里面每一行數據,都是一個異常處理器。
- from指定字節碼索引的開始位置。
- To指定字節碼索引的結束位置。
- Target異常處理的起始位置。
- Type異常類型
也就是說,只要from,to之間出現了異常,就會跳轉到target所指定的位置。
我們看到第一條monitorexit(16)(monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義)在異常表第一條(7-17)的范圍內。如果異常則調到20行。第二個monitorexit同理。
Finally---IOException
通常我們在做一些文件讀取的時候,都會在finally代碼塊中關閉流,以避免內存溢出。關于這個場景,我們再分析一下下面這段代碼的異常表
上面的代碼,捕獲了一個FileNotFoundException異常,然后再finally中捕獲了一個IOException異常。當我們分析字節碼的時候,卻發現了一個有意思的地方,IOException足足出現了三次。
Java編譯器使用了一種比傻的方式來組織finally的字節碼。它分別在try,catch的正常執行路徑上,復制了一份finally代碼。追加在正常執行的后面。同時,再復制一份到其他異常執行邏輯出口處。(相當于對于字節碼來說,如果異常中有finally的異常表。那么它會把自己的異常在try中,catch中各復制一份。怪不得finally一定能走到。有段時間還以為finally是異步達到的必然執行的效果)。
不報錯的除以0
從字節碼可知,0-7行出問題直接走到第9行,也就是finally中。永遠不會執行第8行的ireturn。
字節碼指令---裝箱拆箱
Java中有8種基本數據類型,但是鑒于Java的面線對象特點,它們同樣有著對應的8個包裝類型。比如int和integer,包裝類型的值可以為null(基本類型沒有null值)。而數據庫普遍存在null值,所有實體類中所有屬性應采用包裝類型,很多時候,它們都可以相互賦值。
通過觀察字節碼,我們發現
- 在進行乘法運算的時候,調用了Integer.intValue方法來獲取基本類型的值。
- 賦值操作使用的是Integer.valueOf方法。
- 在方法返回的時候,再次使用了Integer.valueOf方法對結果進行了包裝。
這就是Java中的自動裝箱拆箱的底層實現。
IntegerCache
查看valueOf源碼。發現low和high之間還有一個cache靜態變量
繼續追蹤
發現一般緩存是-128~127.最小值是寫死的,但是最大值可以通過-XX:AutoBoxCacheMax來修改上限。
那么下面一道經典面試題會輸出什么結果呢?
一般不修改參數的情況下就是true,false。
字節碼指令----數組
其實,數組是JVM內置的一種對象類型。這個對象同樣繼承了Object類。可以用代碼解釋。
數組創建
可以看到,新建數組的代碼,被編譯成了newarray指令。(每當遇見new指令后,都會跟一個dup指令)。
具體操作:
4. iconst_0,數組下標為0的常量壓入操作數棧中
5. Sipush,將一個常量為1111的值壓入操作數棧中
8. Iastore,將這個int型變量數組索引為0的位置中
為了支持多種類型的字面量能夠壓入數組,提供了bastore,castore,sastore,iastore等等。
數組訪問
數組的訪問:28~30行實現
- aload_1:該方法的局部變量表中索引為1的引用推送至操作數棧。此處是生成的arr數組引用(意思整個數組先丟到操作數棧里)。
- Iconset_2:將int為2的數字推送至操作數棧
- aload:在數組中取出索引為2的數推送到操作數棧。
獲取數組長度
獲取數組長度指令 arraylength
字節碼指令--foreach
無論是java數組還是List,都可以使用foreach語句進行遍歷。雖然在語言層面它們的表現形式是一致的。但是實際的方法并不同。
數組:將它們代碼解釋成了傳統的變量方式,即:for(int i;i<length;i++)的形式。
List實際是把List對象進行迭代并遍歷,在循環中,使用了Iterator.next()的方法。
使用jd-gui等反編譯工具,可以看到實際代碼的效果
字節碼指令總結
Java的特性非常多,這里不一一列出。但是可以通過查看字節碼的方式,從字節碼的角度分析它的原理,一窺究竟。
本次總結輸入拋磚引玉,給大家一個學習思路。
比如異常處理,finally塊的執行順序,以及隱藏的裝箱拆箱和foreach語法糖的底層實現。
還有字節碼指令。可能幾千行,看起來很嚇人,但是執行速度都是納秒級的。Java的無數框架,包括JDK,也不會為了優化這些行數,就去增加一次Java線程的上下文切換,這個比幾千行字節碼執行慢得多。