本文部分摘自《深入理解 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 關鍵字的語意,兩條指令之間包裹需要同步的指令序列,以實作同步效果
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/246399.html
標籤:Java
