介紹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儲存數據到局部變量表,其余指令基本都是用於操作數棧的。