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儲存資料到區域性變量表,其餘指令基本都是用於運算元棧的。