1. 程式人生 > >Java位元組碼詳解(三)位元組碼指令(轉)

Java位元組碼詳解(三)位元組碼指令(轉)

一、概述

Java虛擬機器採用基於棧的架構,其指令由操作碼和運算元組成。

  • 操作碼:一個位元組長度(0~255),意味著指令集的操作碼個數不能操作256條。
  • 運算元:一條指令可以有零或者多個運算元,且運算元可以是1個或者多個位元組。編譯後的程式碼沒有采用運算元長度對齊方式,比如16位無符號整數需使用兩個位元組儲存(假設為byte1和byte2),那麼真實值是 (byte1 << 8) | byte2。

放棄運算元對齊運算元對齊方案:

  • 優勢:可以省略很多填充和間隔符號,從而減少資料量,具有更高的傳輸效率;Java起初就是為了面向網路、智慧傢俱而設計的,故更加註重傳輸效率。
    劣勢:執行時從位元組碼裡構建出具體資料結構,需要花費部分CPU時間,從而導致解釋執行位元組碼會損失部分效能。

二、指令

大多數指令包含了其操作所對應的資料型別資訊,比如iload,表示從區域性變量表中載入int型的資料到運算元棧;而fload表示載入float型資料到運算元棧。由於操作碼長度只有1Byte,因此Java虛擬機器的指令集對於特定操作只提供有限的型別相關指令,並非為每一種資料型別都有相應的操作指令。必要時,有些指令可用於將不支援的型別轉換為可被支援的型別。

對於byte,short,char,boolean型別,往往沒有單獨的操作碼,通過編譯器在編譯期或者執行期將其擴充套件。對於byte,short採用帶符號擴充套件,chart,boolean採用零位擴充套件。相應的陣列也是採用類似的擴充套件方式轉換為int型別的位元組碼來處理。 下面分門別類來介紹Java虛擬機器指令,都以int型別的資料操作為例。

2.1 棧操作相關

load和store

  • load 命令:用於將區域性變量表的指定位置的相應型別變數載入到棧頂;
  • store命令:用於將棧頂的相應型別資料保入區域性變量表的指定位置;
變數進棧 含義 變數儲存 含義
iload 第1個int型變數進棧 istore 棧頂nt數值存入第1區域性變數
iload_0 第1個int型變數進棧 istore_0 棧頂int數值存入第1區域性變數
iload_1 第2個int型變數進棧 istore_1 棧頂int數值存入第2區域性變數
iload_2 第3個int型變數進棧 istore_2 棧頂int數值存入第3區域性變數
iload_3 第4個int型變數進棧 istore_3 棧頂int數值存入第4區域性變數
lload 第1個long型變數進棧 lstore 棧頂long數值存入第1區域性變數
fload 第1個float型變數進棧 fstore 棧頂float數值存入第1區域性變數
dload 第1個double型變數進棧 dstore 棧頂double數值存入第1區域性變數
aload 第1個ref型變數進棧 astore 棧頂ref物件存入第1區域性變數

const、push和ldc

  • const、push:將相應型別的常量放入棧頂
  • ldc:則是從常量池中將常量
常量進棧 含義
aconst_null null進棧
iconst_m1 int型常量-1進棧
iconst_0 int型常量0進棧
iconst_1 int型常量1進棧
iconst_2 int型常量2進棧
iconst_3 int型常量3進棧
iconst_4 int型常量4進棧
iconst_5 int型常量5進棧
lconst_0 long型常量0進棧
fconst_0 float型常量0進棧
dconst_0 double型常量0進棧
bipush byte型常量進棧
sipush short型常量進棧
常量池操作 含義
ldc int、float或String型常量從常量池推送至棧頂
ldc_w int、float或String型常量從常量池推送至棧頂(寬索引)
ldc2_w long或double型常量從常量池推送至棧頂(寬索引)

pop和dup

  • pop用於棧頂數值出棧操作;
  • dup用於賦值棧頂的指定個數的數值,並將其壓入棧頂指定次數;
棧頂操作 含義
pop 棧頂數值出棧(不能是long/double)
pop2 棧頂數值出棧(long/double型1個,其他2個)
dup 複製棧頂數值,並壓入棧頂
dup_x1 複製棧頂數值,並壓入棧頂2次
dup_x2 複製棧頂數值,並壓入棧頂3次
dup2 複製棧頂2個數值,並壓入棧頂
dup2_x1 複製棧頂2個數值,並壓入棧頂2次
dup2_x2 複製棧頂2個數值,並壓入棧頂3次

swap 棧頂的兩個數值互換,且不能是long/double

注意:dup2對於long、double型別的資料就是一個,對於其他型別的資料,才是真正的兩個,這個的2代表的是2個slot的資料。

2.2 物件相關

欄位呼叫

欄位呼叫 含義
getstatic 獲取類的靜態欄位,將其值壓入棧頂
putstatic 給類的靜態欄位賦值
getfield 獲取物件的欄位,將其值壓入棧頂
putfield 給物件的欄位賦值

方法呼叫

方法呼叫 作用 解釋
invokevirtual 呼叫例項方法 虛方法分派
invokestatic 呼叫類方法 static方法
invokeinterface 呼叫介面方法 執行時搜尋合適方法呼叫
invokespecial 呼叫特殊例項方法 包括例項初始化方法、父類方法
invokedynamic 由使用者引導方法決定 執行時動態解析出呼叫點限定符所引用方法

方法返回

方法返回 含義
ireturn 當前方法返回int
lreturn 當前方法返回long
freturn 當前方法返回float
dreturn 當前方法返回double
areturn 當前方法返回ref

物件和陣列

  • 建立類例項: new
  • 建立陣列:newarray、anewarray、multianewarray
  • 陣列元素 載入到 運算元棧:xaload (x可為b,c,s,i,l,f,d,a)
  • 運算元棧的值 儲存到陣列元素: xastore (x可為b,c,s,i,l,f,d,a)
  • 陣列長度:arraylength
  • 類例項型別:instanceof、checkcast

2.3 運算指令

運算指令是用於對運算元棧上的兩個數值進行某種運算,並把結果重新存入到操作棧頂。Java虛擬機器只支援整型和浮點型兩類資料的運算指令,所有指令如下:

運算 int long float double
加法 iadd ladd fadd dadd
減法 isub lsub fsub dsub
乘法 imul lmul fmul dmul
除法 idiv ldiv fdiv ddiv
求餘 irem lrem frem drem
取反 ineg lneg fneg dneg

其他運算

  • 位移:ishl,ishr,iushr,lshl,lshr,lushr
  • 按位或: ior,lor
  • 按位與: iand, land
  • 按位異或: ixor, lxor
  • 自增:iin
  • 比較:dcmpg,dcmpl,fcmpg,fcmpl,lcmp

2.4 型別轉換

型別轉換用於將兩種不同型別的數值進行轉換。

(1) 對於寬化型別轉換(小範圍向大範圍轉換),無需顯式的轉換指令,並且是安全的操作。各種範圍從小到大依次排序: int, long, float, double。

(2)對於窄化型別轉換,必須顯式地呼叫型別轉換指令,並且該過程很可能導致精度丟失。轉換規則中需要特別注意的是當浮點值為NaN, 則轉換結果為int或long的0。雖然窄化運算可能會發生上/下限溢位和精度丟失等情況,但虛擬機器規範明確規定窄化轉換U不可能導致虛擬機器丟擲異常。

型別轉換指令:i2b, i2c,f2i等等。

2.5 流程控制

控制指令是指有條件或無條件地修改PC暫存器的值,從而達到控制流程的目標

  • 條件分支:ifeq、iflt、ifnull、ifnonnull等
  • 複合分支:tableswitch、lookupswitch
  • 無條件分支:goto、goto_w、jsr、jsr_w、ret

2.6 同步與異常

異常

Java程式顯式丟擲異常: athrow指令。在Java虛擬機器中,處理異常(catch語句)不是由位元組碼指令來實現,而是採用異常表來完成。

同步

方法級的同步和方法內部分程式碼的同步,都是依靠管程(Monitor)來實現的。

Java語言使用synchronized語句塊,那麼Java虛擬機器的指令集中通過monitorenter和monitorexit兩條指令來完成synchronized的功能。為了保證monitorenter和monitorexit指令一定能成對的呼叫(不管方法正常結束還是異常結束),編譯器會自動生成一個異常處理器,該異常處理器的主要目的是用於執行monitorexit指令。

2.7 小結

在基於堆疊的的虛擬機器中,指令的主戰場便是運算元棧,除了load是從區域性變量表載入資料到運算元棧以及store儲存資料到區域性變量表,其餘指令基本都是用於運算元棧的。