1. 程式人生 > 其它 >從位元組碼瞭解Java語言特性

從位元組碼瞭解Java語言特性

位元組碼指令---異常處理

每個時刻正在執行的當前方法就是虛擬機器棧頂的棧幀。方法的執行就對應著棧幀在虛擬機器中入棧和出棧的過程。當一個方法執行完,有兩種情況,一種是正常執行,另一種是異常。

完成出口(返回地址)

正常返回:(呼叫程式計數器中的返回地址)

三部曲:

  1. 恢復上層方法的區域性變量表和運算元棧
  2. 把返回值(如果有的話)壓入呼叫者棧幀的運算元棧中。
  3. 調整程式計數器的值指向方法呼叫指令後面的一條指令。

異常返回

通過異常處理表中的<非棧幀中的>來確定

異常機制

如果熟悉java語言,那麼對以上異常繼承體系一定不會陌生。其中Error和RuntimeException是非檢查型異常,也就是不需要去catch或throw的異常。

異常表

在synchronized生成的位元組碼中,其中包含了兩條monitorexit指令,是為了保證所有的異常條件都能夠退出。可以看到,編譯後的位元組碼,都帶有一個叫Exception table的異常表,裡面每一行資料,都是一個異常處理器。

  1. from指定位元組碼索引的開始位置。
  2. To指定位元組碼索引的結束位置。
  3. Target異常處理的起始位置。
  4. 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值,所有實體類中所有屬性應採用包裝型別,很多時候,它們都可以相互賦值。

通過觀察位元組碼,我們發現

  1. 在進行乘法運算的時候,呼叫了Integer.intValue方法來獲取基本型別的值。
  2. 賦值操作使用的是Integer.valueOf方法。
  3. 在方法返回的時候,再次使用了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行實現

  1. aload_1:該方法的區域性變量表中索引為1的引用推送至運算元棧。此處是生成的arr陣列引用(意思整個陣列先丟到運算元棧裡)。
  2. Iconset_2:將int為2的數字推送至運算元棧
  3. 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執行緒的上下文切換,這個比幾千行位元組碼執行慢得多。