本文部分摘自《深入理解 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 關鍵字的語義,兩條指令之間包裹需要同步的指令序列,以實現同步效果