引言
眾所周知,Java程序是運行在Java虛擬機上的,而這里的“虛擬”是對什么東西進行虛擬呢?答案當然就是對“實體”機進行虛擬啦,虛擬機可以看做是對實體機進行了進一步的封裝和抽象,隱藏了不同實體機之間的差別,從而達成“Write Once,Run AnyWhere”的目標。既然虛擬機是對實體機的虛擬,所以我認為虛擬機和實體機在結構和功能上必然存在某種程度上的對應與關聯。因此我們在學習時應該注意發掘和類比兩者之間的關系。
本著這樣的思想,我們進行Java字節碼指令的學習。JAVA字節碼在JAVA虛擬機中的地位相當于實體機的機器碼,一切在Java虛擬機上運行的程序都要被解釋或編譯成字節碼,一切在實體機上運行的程序最后也都要編譯成機器碼。Java字節碼指令可以對字節碼進行操作,在實體機中對機器碼進行操作的是匯編語言。所以Java字節碼指令對應匯編語言,Java字節碼指令集對應匯編指令集。
字節碼簡介
Java字節碼指令由一個字節長度的,代表某種特定操作含義的數字(操作碼)以及其后的零至多個代表此操作所需參數(操作數)。此外字節碼指令是面向操作數棧的,這里操作數棧在功能上對應實體機的寄存器但是結構上有所區別。
字節碼與數據類型
在字節碼指令集中,大多數指令都對應的其操作所對應的數據類型信息,比如iload表示從局部變量表中加載int型的數據到操作棧中,fload從局部變量表中加載float型的數據到操作棧中...但是由于Java字節碼的操作碼只有一個字節(即0~255),這意味著指令集的操作碼總數不可能超過256條。所以如果要求Java運行時所有的數據類型都有對應的與數據類型相關的指令去支持的話,操作碼的總數將超過256條。所以JAVA字節碼指令集被設計為Not Orthogonal(非完全獨立),即并非每種數據類型和每種操作都有對應的指令,有一些指令可以在必要的時候將一些不被支持的數據類型轉換為被支持的數據類型。我們可以以數據類型為列,操作指令為行制作一張表,其中為空的項即說明虛擬機不支持對這種數據類型進行這項操作。
加載和存儲指令
加載和存儲指令用于將數據在幀棧中的局部變量表和操作數棧之間傳輸。
- 將一個局部變量表加載到操作數棧:
iload、iload_<n>
、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>
、aload、aload_<n>
。 - 將一個數值從操作數棧儲存到局部變量表:
istore,istore_<n>
,lstore,lstore_<n>
,fstore,fstore_<n>
,dstore,dstore_<n>
,astore,astore_<n>
。 - 將一個常量加載到操作數棧:
bipush,sipush,lde,lde_w,ldc2_w,aconst_null,iconst_ml,iconst_<i>,lconst_<i>,fconst_<i>,dconst_<i>。
- 拓充局部變量表的訪問引索的指令:wide
運算指令
運算指令用于對操作數棧上的值進行某種特定的運算。
- 加法運算:iadd,ladd,fadd,dadd。
- 減法運算:isub,lsub,fsub,dsub。
- 乘法運算:imul,lmul,fmul,dmul。
- 除法運算:idiv,ldiv,fdiv,ddiv。
- 求余指令:irem,lrem,frem,drem。
- 取反指令:imeg,lmeg,fmeg,dmeg。
- 位移指令:ishl,ishr,iushr,lshl,lshr,lushr。
- 按位或指令:ior,lor。
- 按位與指令:iand,land。
- 按位異或指令:ixor,lxor。
- 局部變量自增指令:iinc。
- 比較指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp。
注:只有在除法指令(idiv,ldiv)和求余指令(irem,lrem)當出現除數為零時會導致虛擬機拋出AirtmeticException異常,其余整形和浮點型運算場景都不會拋出異常
類型轉換指令
類型轉換指令可以將兩種不同數值類型進行相互轉換。
Java虛擬機天然支持基本數據類型的寬化類型轉換,例如int到long、flost、double等。
對于窄化數據類型轉化則必須用顯示的轉換指令:
- i2b(int -> boolean)
- i2c(int -> char)
- i2s(int -> short)
- l2i(long -> int)
- f2i(float -> int)
- f2l(float -> long)
- d2i(double -> int)
- d2l(double -> long)
- d2f(double -> float)
幾點說明: - int/long 類型窄化轉換為整數類型T時,轉換過程為丟棄除最低位N(T的數據類型長度)個字節以外的內容。
- 浮點值窄化轉換為整數類型T(int/long)時:
if(浮點值==NaN){
result = 0;
}else{
value = [浮點值]; //向下取整
if(T.min <= value <= T.max){ //value在T的表示范圍內
result = value;
}else{
if(value > 0) result = T.max;
if(value < 0) result = T.min;
}
}
對象創建與訪問指令
- 創建類實例的指令:new
- 創建數組的指令:newarray,anewarray,multianewarray
- 訪問類字段(static字段)和實例字段(非static字段)的指令:getfield,putfield,getstatic,putstatic
- 將一個數組元素加載到操作數棧的指令:
baload,caload,saload,iaload,faload,daload,aaload - 將一個操作數棧的值存儲到數組元素中的指令:
bastore,castore,iastore,sastore,fastore,fastore,dastore,aastore - 取數組長度的指令:arraylength
- 檢查類實例類型的指令:instanceof,checkcast
操作數棧管理指令
- 將一個操作數棧的棧頂一個或兩個元素出棧:pop、pop2。
- 復制棧頂一個或兩個數值并將復制值或雙份的復制值重新壓入棧頂:dup、dup2、dup_x1,dup2_x1,dup_x2,dup2_x2。
- 將棧頂端的兩個數值交換:swap。
控制轉移指令
控制轉移指令可以讓Java虛擬機有條件或者無條件的從指定的位置而不是控制轉移指令的下一條指令繼續執行程序。
- 條件分支:
ifeq,ifit,ifle,ifgt,ifnull,ifnonnull,if_icmpeq,if_icmpne,if_icmplt,if_icmpgt,if_icmple,if_icmpge,if_acmpeq,if_acmpne。 - 復合條件分支:tableswitch,lookupswitch。
- 無條件分支:gosto,goto_w,jsr,jsr_w,ret。
方法調用和返回指令
- invokevirtual:用于調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派)。
- invokeinterface:用于調用接口方法,它在運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用。
- invokespecial:用于調用一些需要特殊處理的實例方法,包括實例的初始化方法,私有方法和父類方法。
- invokestatic:用于調用類方法(static方法)
- invokedynamic:用于運行時動態解析出調用點限定符所應用的方法,并執行該方法。(前面的分派邏輯都固化在虛擬機內部,而該指令的分派邏輯是由用戶自定義)。
- 方法返回指令:ireture(返回類型是int,short,byte,char,boolean時),lreturn,freturn,dreturn,areturn,另外還有一條return供void方法、實例/類/接口的初始化方法使用。
異常處理指令
顯式拋出異常指令:athrow
同步指令
- monitorenter,monitorexit
小練習
我們拿Java里面比較經典的i++和++i問題來做個練習,熟悉下用字節碼分析問題:
Test Case for ++i:
public class Test_1 {
public static void main( String[] argv )
{
int value = 0;
value = ++value;
System.out.println(value);
}
}
運行結果:1。
對應部分字節碼及分析:
Code:
0: iconst_0 //將0加載到棧頂
1: istore_1 //將0存儲到變量value
2: iinc 1, 1 //value在局部變量表自增為1,(此處為虛指令,真實的變量操作要靠load和store指令)
5: iload_1 //將value的值加載到棧頂
6: istore_1 //將棧頂的內容保存到變量value,value=1
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
14: return
Test Case for i++:
public class Test {
public static void main( String[] argv )
{
int value = 0;
value = value++;
System.out.println(value);
}
}
運行結果:0
字節碼及分析:
Code:
0: iconst_0 //將0加載到棧頂
1: istore_1 //將0存儲到變量value
2: iload_1 //將value的值加載到棧頂,棧頂為0
3: iinc 1, 1 //value在局部變量表自增為1
6: istore_1 //將棧頂的內容保存到變量value,value=0
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
14: return
以上通過字節碼分析對這個問題無疑有了更深層次的理解。
我的博客:博客