一、概述
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儲存數據到局部變量表,其餘指令基本都是用於操作數棧的。
原文:Jvm系列2-字節碼指令