1. 程式人生 > >JVM 位元組碼指令

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\_\、lload、lload\_\、fload、fload\_\、dload、dload\_\、aload、aload\_\ - 將一個數值從運算元棧儲存到區域性變量表:istore、istore\_\、lstore、lstore\_\、fstore、fstore\_\、dstore、dstore\_\、astore、astore\_\ - 將一個常量載入到運算元棧:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_\、lconst\_\、fconst\_\、dconst\_\ - 擴充區域性變量表的訪問索引的指令:wide 上面所列舉的指令助記符中,有一部分是以尖括號結尾,如 iload\_\,實際上代表了 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 關鍵字的語義,兩條指令之間包裹需要同步的指令序列,以實現同步效