說明,本文的目的在於從宏觀邏輯上介紹清楚絕大多數的字節碼指令的含義以及分類
只要認真閱讀本文必然能夠對字節碼指令集有所了解
如果需要了解清楚每一個指令的具體詳盡用法,請參閱虛擬機規范
指令簡介
計算機指令就是指揮機器工作的指示和命令,程序就是一系列按一定順序排列的指令,執行程序的過程就是計算機的工作過程。
通常一條指令包括兩方面的內容: 操作碼和操作數,操作碼決定要完成的操作,操作數指參加運算的數據及其所在的單元地址。
虛擬機的字節碼指令亦是如此含義
class文件相當於JVM的機器語言
class文件是源代碼信息的完整表述
方法內的代碼被保存到code屬性中,字節碼指令序列就是方法的調用過程
Java虛擬機的指令由一個字節長度的、代表着某種特定操作含義的操作碼(opcode)
以及跟隨其后的零至多個代表此操作所需參數的操作數(operand)所構成
虛擬機中許多指令並不包含操作數.只有一個操作碼。
如果忽略異常處理,執行邏輯類似
do{ 自動計算pc寄存器以及從pc寄存器的位置取出操作碼; if(存在操作數){ 取出操作數; } 執行操作碼所定義的操作; }while(處理下一次循環);
操作數的數量以及長度取決於操作碼,如果一個操作數的長度超過了一個字節,那么它將大端排序存儲,即高位在前的字節序。
例如,如果要將一個16位長度的無符號整數使用兩個無符號字節存儲起來(將它們命名為byte]和byte2 )
那這個16位無符號整數的值就是: (bytel<<8) | byte2.
字節碼指令流應當都是單字節對齊的,只有,tableswitch和lookupswitch兩個指令例外 這倆貨是4字節為單位的
限制了操作碼長度為一個字節 0~255, 但是也就導致操作碼個數不能超過256
放棄編譯后代碼的操作數對齊 也就省略很多填充和間隔符號
限制長度和放棄對齊也盡可能的讓編譯后的代碼短小精干
但是如果向上面那樣如果操作碼處理超過一個字節的數據時,就必須在運行時從字節流中重建出具體數據結構,將會有一定程度的性能損失
指令詳解
說明:
操作碼一個字節長度,也就是8位二進制數字,也就是兩位十六進制數字
class文件只會出現數字形式的操作碼
但是為了便於人識別,操作碼有他對應的助記符形式
接下來所有的指令的說明,都是以助記符形式表達的
但是要明確,實際的執行運行並不存在助記符這些東西,都是根據操作碼的值來執行
指令本身就是為了功能邏輯運算
運算自然要處理數據
所以說指令的設計是邏輯功能點與數據類型的結合
接下來先看下有哪些數據類型和邏輯功能點
數據類型
上一篇文章中已經說明JVM支持的數據類型
共有9中基本類型
對於基本類型 指令在設計的時候都用一個字母縮寫來指代(boolean除外)
byte | short | int | long | float | double | char | reference | boolean |
b | s | i | l | f | d | c | a | 無 |
邏輯功能
加載存儲指令 |
算數指令 |
類型轉換指令 |
對象的創建於操作 |
操作數棧管理指令 |
控制轉移指令 |
方法調用和返回指令 |
拋出異常 |
同步 |
指令基本上就是圍繞着上面的邏輯功能以及數據類型進行設計的
當然
也有一些並沒有明確用字母指代數據類型,比如arraylength 指令,並沒有代表數據類型的特殊字符,操作數只能是一個數組類型的對象
另外還有一些,比如無條件跳轉指令goto 則是與數據類型無關的
接下來將會從各個維度對絕大多數指令進行介紹
注意: 在不同的分類中,有些指令是重復的,因為有很多操作是需要處理數據的
也就是說數據類型相關的指令里面可能跟很多邏輯功能點相關聯,比如 加載存儲指令,可以加載int 可以加載long等
他在我接下來的說明中,可能不僅僅會出現在數據類型相關的指令中
也會出現在加載存儲指令的介紹中,請不要疑惑
就是要從多維度介紹這些指令,才能更好地理解他們
指令-相關計算機英語詞匯含義
push | push | 按 推動 壓入 |
load | load | 加載 裝載 |
const | const | 常數,不變的 |
store | store | 存儲 保存到 |
add | add | 加法 |
sub | subduction | 減法 |
mul | multiplication | 乘法 |
div | division | 除法 |
inc | increase | 增加 |
rem | remainder | 取余 剩下的留下的 |
neg | negate | 取反 否定 |
sh | shift | 移位 移動變換 |
and | and | 與 |
or | or | 或 |
xor | exclusive OR | 異或 |
2 | to | 轉換 轉變 變成 |
cmp | compare | 比較 |
return | return | 返回 |
eq | equal | 相等 |
ne | not equal | 不相等 |
lt | less than | 小於 |
le | less than or equal | 小於等於 |
gt | greater than | 大於 |
ge | greater than or equal | 大於等於 |
if | if | 條件判斷 如果 |
goto | goto | 跳轉 |
invoke | invoke | 調用 |
dup | dump | 復制 拷貝 卸下 丟下 |
指令-數據類型相關的指令
java中的操作碼長度只有個字節,所以必然,並不會所有的類型都有對應的操作
Java虛擬機指令集對於特定的操作只提供了有限的類型相關指令
有一些單獨的指令可以再必要的時候用來將一些不支持的類型轉換為可支持的類型
|
下表中最左邊一列的T表示模板,只需要用數據類型的縮寫,替換掉T 就可以得到對應的具體的指令
如果下表中為空,說明對這種數據類型不支持這種類型的操作
操作碼/類型 | 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類型數據
在處理boolean byte short char類型的數組時,也會轉換為使用對應的int類型的字節碼指令來處理
另外需要格外注意的是,上表是為了呈現部分與數據類型相關聯的操作碼
並不是說所有的操作碼都在上表中,僅僅是和數據類型相關聯的才出現在了上表中
|
實際類型與運算類型的對應關系如下,分類后面會說到
實際類型 | 運算類型 | 分類 |
boolean | int | 1 |
int | int | 1 |
byte | int | 1 |
short | int | 1 |
int | int | 1 |
float | float | 1 |
reference | reference | 1 |
returnAddress | returnAddress | 1 |
long | long | 2 |
double | double | 2 |
按照邏輯功能進行划分
加載存儲指令
算數指令
運算后的結果自動入棧
運算或算術指令用於對兩個操作數棧上的值進行某種特定運算,並把結果重新存入到操作棧頂.
算術指令分為兩種:整型運算的指令和浮點型運算的指令.
無論是哪種算術指令,都使用Java虛擬機的數據類型
由於沒有直接支持byte、short、char和boolean類型的算術指令,使用操作int類型的指令代替.
|
加法指令: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
|
再次強調 加add 減sub 乘mul 除div 求余rem 取反neg 移位sh l r表示左右 與and 或or 異或xor 自增inc cmp比較 |
加 減 乘 除 求余 取反 支持 <int i long l float f double d> 四種類型 理解點:常用操作支持四種常用類型 byte short char boolean使用int 移位運算與按位與或異或運算 支持< int i long l > 理解點: 移位與位運算支持整型,byte short char boolean使用int 另外還有long 自增支持< int i > |
補充說明:
關於移位運算,
左移只有一種:
規則:丟棄最高位,往左移位,右邊空出來的位置補0
右移有兩種:
1. 邏輯右移:丟棄最低位,向右移位,左邊空出來的位置補0
2. 算術右移:丟棄最低位,向右移位,左邊空出來的位置補原來的符號位(即補最高位)
移位運算的u表示的正是邏輯移位d 和f開頭 分別代表double 和float的比較 cmpg 與cmpl 的唯一區別在於對NaN的處理,更多詳細內容可以查看虛擬機規范的相關指令 lcmp 比較long類型的值 |
類型轉換指令
類型轉換指令可以將兩種不同的數值類型進行相互轉換。
這些轉換操作一般用於實現用戶代碼中的顯式類型轉換操作
或者用來解決字節碼指令集不完備的問題
因為數據類型相關指令無法與數據類型一一對應的問題,比如byte short char boolean使用int, 所以必須要轉換
|
分為寬化 和 窄化 含義如字面含義,存儲長度的變寬或者變窄 寬化也就是常說的安全轉換,不會因為超過目標類型最大值丟失信息 窄化則意味着很可能會丟失信息 寬化指令和窄化指令的形式為 操作類型 2 (to) 目標類型 比如 i2l int 轉換為long |
寬化指令
int類型到long、float或者double類型
long類型到float、double類型
float類型到double類型
i2l、i2f、i2d
l2f 、l2d
f2d
|
窄化指令 int類型到byte short char類型 long類型到int類型 float類型到int或者long類型 從double類型到int long 或者float類型 i2b 、i2s 、i2c l2i f2i 、f2l d2i 、d2l 、d2f |
對象的創建與訪問
實例和數組都是對象 但是Java虛擬機對類實例和數組的創建使用了不同的字節碼指令 |
涉及到對象的創建與訪問的相關操作有: 1.創建實例對象/數組 2.訪問實例變量和類變量 3.加載與存儲,對於類實例屬於引用類型存取使用加載存儲指令,所以此處只有數組有相關操作了 4.還有一些附屬信息 數組長度以及檢查類實例或者數組類型 |
創建類實例 : new 創建數組的指令 : newarray 分配數據成員類型為基本數據類型的新數組 anewarray 分配數據成員類型為引用類型的新數組 multianewarray 分配新的多維數組 |
類變量聲明的時候使用static關鍵字 訪問與存儲類中的靜態字段也是使用static關鍵字 getstatic 從類中獲取靜態字段 putstatic 設置類中靜態字段的值 普通的成員實例變量使用field指代 getfield 從對象中獲取字段值 putfield 設置對象中的字段的值 |
訪問與存儲之前介紹過 使用的load 和store 數組也是對象 引用使用a來表示 所以對於數組的存取和訪問指令 使用 類型+a+load 或者store 的形式 把一個數組元素加載到操作數棧的指令: byte char short int long float double reference 對應的指令分別是 baload caload saload iaload laload faload daload aaload 把一個操作數棧的值存儲到數組元素中的指令:
byte char short int long float double reference
對應的指令分別是:
bastore castore sastore iastore lastore fastore dastore aastore
|
獲取數組長度的指令 arraylength 檢查類實例或者數組類型的指令 instanceof checkcast |
操作數棧管理指令
控制轉移指令
控制轉移指令可以讓Java虛擬機有條件或者無條件的從指定的位置指令繼續執行程序 而不是當前控制轉移指令的下一條 |
控制轉移指令包括
條件轉移 復合條件轉移以及無條件轉移
boolean byte short char都是使用int類型的比較指令
long float double 類型的條件分支比較,會先執行相應的比較運算指令,運算指令會返回一個整型數值到操作數棧中
隨后在執行int類型的條件分支比較操作來完成整個分支跳轉
顯然,虛擬機會對int類型的支持最為豐富
所有的int類型的條件分支指令進行的都是有符號的比較
|
long float double 類型的比較指令 lcmp fcmpl fcmpg dcmpl dcmpg 這五個都比較棧頂上面兩個 指定類型的元素,然后將結果 [-1 0 1] 壓入棧頂 cmpl與cmpg區別在於對NaN的處理,有興趣的可以查看Java虛擬機規范 |
條件跳轉指令
接下來這六個也就是上面說的配合long float 和double類型條件分支的比較
他們會對當前棧頂元素進行操作判斷,只有棧頂的一個元素作為操作數
ifeq 當棧頂int類型元素 等於0時 ,跳轉
ifne 當棧頂int類型元素 不等於0 時,跳轉
iflt 當棧頂int類型元素 小於0 時,跳轉
ifle 當棧頂int類型元素 小於等於0 時,跳轉
ifgt 當棧頂int類型元素 大於0 時,跳轉
ifge 當棧頂int類型元素 大於等於0 時,跳轉
|
類似上面的long float double
int類型 和 reference 當然也有對兩個操作數的比較指令,而且還一步到位了
if_icmpeq 比較棧頂兩個int類型數值的大小 ,當前者 等於 后者時,跳轉
if_icmpne 比較棧頂兩個int類型數值的大小 ,當前者 不等於 后者時,跳轉
if_icmplt 比較棧頂兩個int類型數值的大小 ,當前者 小於 后者時,跳轉
if_icmple 比較棧頂兩個int類型數值的大小 ,當前者 小於等於 后者時,跳轉
if_icmpge 比較棧頂兩個int類型數值的大小 ,當前者 大於等於 后者時,跳轉
if_icmpgt 比較棧頂兩個int類型數值的大小 ,當前者 大於 后者時,跳轉
if_acmpeq 比較棧頂兩個引用類型數值的大小 ,當前者 等於 后者時,跳轉
if_acmpne 比較棧頂兩個引用類型數值的大小 ,當前者 不等於 后者時,跳轉
|
復合條件跳轉指令 tableswitch switch 條件跳轉 case值連續 lookupswitch switch 條件跳轉 case值不連續 |
無條件轉移指令 goto 無條件跳轉 goto_w 無條件跳轉 寬索引 jsr SE6之前 finally字句使用 跳轉到指定16位的offset,並將jsr下一條指令地址壓入棧頂 jsr_w SE6之前 同上 寬索引 ret SE6之前返回由指定的局部變量所給出的指令地址(一般配合jsr jsr_w使用) w同局部變量的寬索引含義 |
方法調用和方法返回指令
方法調用和方法返回指令 |
方法調用分為
實例方法接口方法 調用父類私有實力初始化等特殊方法,類靜態方法等
以下5條指令用於方法調用:
invokevirtual指令用於調用對象的實例方法
invokeinterface指令用於調用接口方法,它會在運行時搜索由特定對象所實現的這個接口方法,並找出適合的方法進行調用。
invokespecial指令用於調用一些需要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法。
invokestatic指令用於調用類方法(static方法)
invokedynamic 調用動態鏈接方法 比較復雜,稍后有時間會專門講解
|
方法的調用與數據類型無關 但是方法的返回指令根據返回值類型進行區分 ireturn boolean byte char short int類型使用 lreturn long freturn float dreturn double areturn reference return void方法 實例初始化方法(構造方法) 類和接口的類初始化方法 |
異常指令
異常處理指令 |
Java程序中顯式拋出異常的操作 throw語句,都是由athrow 指令來實現的 除了throw語句顯式的拋出異常情況之外,Java虛擬機規范還規定了許多運行時異常 會在其他Java虛擬機指令檢測到異常情況時,自動拋出 |
同步指令
同步指令 |
同步一段指令集序列通常是由Java語言中的synchronized 語句塊來表示的, Java虛擬機的指令集中有monitorenter monitorexit (monitor +enter/exit) |
至此,虛擬機中的指令集的大致基本設計邏輯以及意圖已經基本介紹清楚了,如需要更深一步的了解,請查看虛擬機規范