JVM 位元組碼指令

本文部分摘自《深入理解 Java 虛擬機》

簡介

Java 虛擬機的指令由操作碼 + 操作數組成,其中操作碼是代表某種特定操作含義的數字,長度為一個位元組,而操作數就是此操作所需的一個或多個參數。由於 Java 虛擬機採用面向操作數棧而非暫存器的架構,所以大多數指令都不包括操作數,只有一個操作碼

既然限制了 JVM 操作碼的長度為一個位元組(0 ~ 255),也意味著指令集的操作碼總數不超過 256 條。Class 文件格式放棄了編譯後程式碼的操作數長度對齊,因此虛擬機在處理那些超過一個位元組的數據時,不得不在運行時從位元組中重建出具體數據的結構,這會損失一些性能,但也省略了大量的填充和間隔符號,儘可能得到短小精悍的編譯程式碼

位元組碼和數據類型

在 Java 虛擬機的指令集中,大多數指令都包含其操作所對應的數據類型資訊,每種數據類型都有特殊的字元來表示。但 Java 虛擬機的操作碼長度只有一個位元組,如果為每一種與數據類型相關的指令都支援 Java 虛擬機所有運行時數據類型的話,那指令的數量恐怕就會超過一位元組所能表示的數量範圍了

因此,Java 虛擬機對於特定的操作只提供了有限的類型相關指令去支援它,即並非每種數據類型和每一種操作都有對應的指令。下表就是特定操作與其支援數據類型的關係圖,指令中的 T 可以替換為對應的數據類型,空格表示不支援這種數據類型執行這項操作

opcode byte short int long float double char reference
Tipush bipush sipush
Tconst iconst lconst fconst dconst aconst
Tload iload lload fload dload aload
Tstore istore lstore fstore dstore astore
Tinc iinc
Taload baload saload iaload laload faload daload caload aaload
Tastore bastore sastore iastore lastore fastore dastore castore aastore
Tadd iadd ladd fadd dadd
Tsub isub lsub fsub dsub
Tmul imul lmul fmul dmul
Tdiv idiv ldiv fdiv ddiv
Trem irem lrem frem drem
Tneg ineg lneg fneg dneg
Tshl ishl lshl
Tshr ishr lshr
Tushr iushr lushr
Tand iand land
Tor ior lor
Txor ixor lxor
i2T i2b i2s i2l i2f i2d
l2T l2i l2f l2d
f2T f2i f2l f2d
d2T d2i d2l d2f
Tcmp lcmp
Tcmpl fcmpl dcmpl
Tcmpg fcmpg dcmpg
if_TcmpOP if_icmpOP if_acmpOP
Treturn ireturn lreturn freturn dreturn areturn

可以發現,大部分指令都沒有支援 byte、char、short、boolean,編譯器會在編譯期或運行期將 byte 和 short 類型的數據帶符號擴展為相應的 int 類型數據,將 boolean 和 char 類型數據零位擴展為相應的 int 類型數據,然後使用對應 int 類型的位元組碼指令來處理。因此,大多數對於 boolean、byte、short 和 char 類型數據的操作,實際上都是轉換成 int 類型再進行操作

載入和存儲指令

載入和存儲指令用於將數據在棧幀中的局部變數和操作數棧之間來回傳輸,這類指令包括:

  • 將一個局部變數載入到操作數棧: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、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
  • 擴充局部變數表的訪問索引的指令:wide

上面所列舉的指令助記符中,有一部分是以尖括弧結尾,如 iload_<n>,實際上代表了 iload_0、iload_1、iload_2 和 iload_3 這幾條指令。iload_0 等價於 iload 0,同理,iload_1 等價與 iload 1 ……,它們省略了顯示的操作數,不需要進行取操作數的動作,除此之外,它們的語義和原生的通用指令是完全一致

運算指令

算術指令用於對兩個操作數棧上的值進行某種特定運算,並把結果重新存入到操作數棧頂。所有的算術指令包括:

  • 加法指令: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 支援小範圍類型向大範圍類型的安全轉換,例如 int 到 long、float、double,與之相反的就必須顯式地使用轉換指令完成,這些指令包括 i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f 轉換過程可能會導致數值的精度丟失

對象創建與訪問指令

雖然類實例和數組都是對象,但 Java 虛擬機對類實例和數組的創建與操作使用了不同的位元組碼指令。對象創建後,就可以通過對象訪問指令獲取對象實例或者數組實例中的欄位或者數組元素:

  • 創建類實例指令:new
  • 創建數組的指令:newarray、anewarray、multianewarray
  • 訪問類欄位(static 欄位、或者稱為類變數)和實例欄位(非 static 欄位,或被稱為實例變數)的指令:getfield、putfield、getstatic、putstatic
  • 把一個數組元素載入到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
  • 將一個操作數棧的值存儲到數組元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
  • 取數組長度的指令:arraylength
  • 檢查類實例類型的指令:instanceof、checkcast

操作數棧管理指令

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

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

控制轉移指令

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

  • 條件分支:ifeq、iflt、ifle、ifne、ifgt、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

方法調用和返回指令

方法調用指令和數據類型無關,而方法返回指令是根據返回值的類型區分的

  • invokevirtual 指令:用於調用對象的實例方法,根據對象的實際類型進行分派
  • invokeinterface 指令:用於調用介面方法,它會在運行時搜索一個實現了這個介面方法的對象,找出合適的方法進行調用
  • invokespecial 指令:用於調用一些需要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法
  • invokestatic 指令:用於調用類靜態方法
  • invokedynamic 指令:用於在運行時動態解析出調用點限定符所引用的方法,並執行

異常處理指令

在 Java 程式中顯式地拋出異常的操作(throw)都由 athrow 指令來實現,除了用 throw 語句顯式拋出異常的情況外,Java虛擬機規範還規定了許多運行時異常會在其他 Java 虛擬機指令檢測到異常狀況時自動拋出。對於處理異常(catch)操作,不是由位元組碼指令來實現,而是採用異常表

同步指令

Java 虛擬機可以支援方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都使用管程(Monitor,更常見的是直接稱它為鎖)來實現

方法級的同步是隱式的,無須通過位元組碼指令是實現,它實現在方法調用和返回操作之中。虛擬機可以從方法常量池中的方法表結構中的 ACC_SYNCHRONIZED 訪問標誌得知一個方法是否被聲明為同步方法。當方法被調用時,調用指令會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行執行緒就要去先成功持有管程。在方法執行期間,執行執行緒持有管程,其他任何執行緒都無法再獲取到同一個管程。如果一個同步方法執行期間拋出異常,並在方法內部無法處理,此時同步方法所持有的管程將在異常拋到同步方法邊界之外自動釋放

同步一段指令集序列通常是由 Java 語言中的 synchronized 語句塊來表示的,Java 虛擬機的指令集中有 monitorenter 和 monitorexit 兩條指令來支援 synchronized 關鍵字的語義,兩條指令之間包裹需要同步的指令序列,以實現同步效果

Tags: