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 關鍵字的語義,兩條指令之間包裹需要同步的指令序列,以實現同步效果


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章