1. 程式人生 > >Java字節碼指令

Java字節碼指令

ifnull IT 實例初始化 das 行為 傳輸 多個 BE 復制

1. 簡介

Java虛擬機的指令由一個字節長度的、代表著某種特定操作含義的數字(稱為操作碼)以及跟隨其後的零至多個代表此操作所需參數(稱為操作數)而構成。

由於Java虛擬機采用面向操作數棧而不是寄存器的架構,所以大多數的指令都不包含操作數,只有一個操作碼。

Java虛擬機操作碼的長度為一個字節(即0~255),這意味著指令集的操作碼總數不可能超過256條。

2. 字節碼和數據類型

在Java虛擬機的指令集中,大多數的指令都包含了其操作所對應的數據類型信息。例如,iload指令用於從局部變量中加載int型的數據到操作數棧中,而fload指令加載的則是float類型的數據。

大部分與數據類型相關的字節碼指令,它們的操作碼助記符都有特殊的字符來表明專門為哪種數據類型服務:i代表對int類型的數據操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令的助記符中沒有明確地指明操作類型的字母,如arraylength指令。

如果每一種與數據類型相關的指令都支持Java虛擬機所有運行時數據類型的話,那指令的數量就會超出一個字節所能標示的數量範圍了。因此,Java虛擬機的指令集對於特定的操作只提供了有限的類型相關指令去支持它,換句話說,指令集將會故意被設計成非完全獨立的,有一些單獨的指令可以在必要的時候用來將一些不支持的類型轉換為可被支持的類型。

3. 字節碼分類介紹

3.1 加載和存儲指令

加載和存儲指令用於將數據在棧幀中的局部變量表和操作數棧之間來回傳輸。

  • 將一個局部變量加載到操作棧:iload,iload_<n>
  • 將一個數值從操作數棧存儲到局部變量表:istore,istore_<n>
  • 將一個常量加載到操作數棧:bipush
  • 擴充局部變量表的訪問索引的指令:wide

其中iload_<n>代表了一組指令,代表了iload_0,iload_1,iload_3這幾條指令。這幾組指令都是某個帶有一個操作數的通用指令的特殊形式,對於這若幹組特殊指令來說,他們省略了顯式的操作數,不需要進行取操作數的動作。實際上操作數就隱含在指令中。例如,iload_0的語義與操作數為0時的iload指令語義完全一致。

3.2 運算指令

運算或算術指令用於對兩個操作數棧上的值進行某種特定運算,並把結果重新存入到操作棧頂。大體上算術指令可以分為兩種:對整型數據進行運算的指令與對浮點型數據進行運算的指令。無論是哪種算術指令,都使用Java虛擬機的數據類型,由於沒有直接支持byte、short、char和boolean類型的算術指令,對於這類數據的運算,應使用操作int類型的指令代替。整數與浮點數的算術指令在溢出和被零除的時候也有各自不同的行為表現,所有的算術指令如下:

  • 加法指令: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
  • 局部變量自增指令:iinc
  • 比較指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp

數據運算可能會導致溢出,例如:兩個很大的正整數相加,結果可能會是一個負數,其實Java虛擬機規範沒有明確定義過整型數據溢出的具體運算結果,僅規定了在處理整型數據時,只有除法指令(idiv和ldiv)以及求余指令(irem和lrem)中當出現除數為零時會導致虛擬機拋出ArithmeticException溢出,其余任何整型數運算場景都不應該拋出運行時異常。

3.3 類型轉換指令

類型轉換指令可以將兩種不同的數值類型進行相互轉換,這些轉換操作一般用於實現用戶代碼中的顯式類型轉換操作。

Java虛擬機直接支持以下數值類型的寬化類型轉換(Widening Numeric Conversions,即小範圍類型向大範圍類型的安全轉換)

  • int類型到long,float或double
  • long類型到float,double
  • float類型到double

相對的,處理窄化類型轉換(Narrowing Numberic Conversions)時,必須顯式地使用轉換指令來完成,這些轉換執行包括:i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l和d2f。窄化類型轉換可能會導致轉換結果產生不同的正負號、不同的數量級的情況,轉換過程很可能會導致數值的精度丟失。

盡管數據類型窄化轉換可能會發生上限溢出、下限溢出和精度丟失等情況,但是Java虛擬機規範中明確規定數值類型的窄化轉換指令永遠不可能導致虛擬機拋出運行時異常。

3.4 對象創建和訪問指令

雖然類實例和數組都是對象,但Java虛擬機對類實例和數組的創建與操作使用了不同的字節碼指令,指令如下:

  • 創建類實例的指令:new
  • 創建數組的指令:newarray,anewarray,multianewarray
  • 訪問類字段和實例字段的指令:getfield,putfield,getstatic,putstatic
  • 把一個數組元素加載到操作數棧的指令:baload,caload,saload,iaload,laload,faload,daload,aaload
  • 將一個操作數棧的值存儲到數組元素中的指令:bastore,castore,sastore,iastorefastore,dastore,aastore
  • 取數組長度的指令:arraylength
  • 檢查類實例類型的指令:instanceof,checkcast

3.5 操作數棧管理指令

如同操作一個普通數據結構中的堆棧那樣,Java虛擬機提供了一些用於直接操作操作數棧的指令,包括:

  • 將操作數棧的棧頂一個或兩個元素出棧:pop,pop2
  • 復制棧頂一個或兩個數值並將復制值或雙份的復制值重新壓入棧頂:dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2
  • 將棧最頂端的兩個數值互換:swap

3.6 控制轉移指令

控制轉移指令可以讓Java虛擬機有條件或無條件地從指定的位置指令而不是控制轉移指令的下一條指令繼續執行程序,從概念模型上理解,可以認為控制轉移指令就是在有條件或無條件地修改PC寄存器的值。控制轉移指令如下:

  • 條件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull,if_icmpeq,if_icmpne,if_icmplt,if_icmpgt,if_icmple,if_icmpge,if_acmpeq和if_acmpne
  • 復合條件分支:tableswitch,lookupswitch
  • 無條件分支:goto,goto_w,jsr,jsr_w,ret

3.7 方法調用和返回指令

  • invokevirtual:指令用於調用對象的實例方法,根據對象的實際類型進行分派,這也是Java語言中最常見的方法分派方式‘;
  • invokeinterface:指令用於調用接口方法,它會在運行時搜索一個實現了這個接口方法的對象,找到合適的方法進行調用;
  • invokespecial:用於調用一些需要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法;
  • invokestatic:用於調用類方法(static方法)
  • invokedynamic:用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法,前面4條調用指令的分派邏輯都固化在Java虛擬機內部,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。

方法調用指令與數據類型無關,而方法返回指令時根據返回值的類型區別的,包括ireturn(返回值是boolean,byte,char,short和int),lreturn,freturn,dreturn和areturn,另外還有一條return指令供void方法,實例初始化方法以及類和接口的類初始化方法使用。

3.8 異常處理指令

在Java程序中顯式拋出異常的操作(throw語句)都由athrow指令來實現。

Java虛擬機規範還規定了許多運行時異常會在其他Java虛擬機指令檢測到異常狀況時自動拋出。

而在Java虛擬機中,處理異常(catch語句)不是由字節碼指令來實現的,而是采用異常表來完成。

3.9 同步指令

Java虛擬機可以支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支持的。

Java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義。

編譯器必須確保無論方法通過何種方式完成,方法中調用過的每條monitorenter指令都必須執行其對於的monitorexit指令,而無論這個方法是正常結束還是異常結束。

Java字節碼指令