[三] java虛擬機 JVM字節碼 指令集 bytecode 操作碼 指令分類用法 助記符


說明,本文的目的在於從宏觀邏輯上介紹清楚絕大多數的字節碼指令的含義以及分類
只要認真閱讀本文必然能夠對字節碼指令集有所了解
如果需要了解清楚每一個指令的具體詳盡用法,請參閱虛擬機規范

指令簡介

計算機指令就是指揮機器工作的指示和命令,程序就是一系列按一定順序排列的指令,執行程序的過程就是計算機的工作過程。
通常一條指令包括兩方面的內容: 操作碼和操作數,操作碼決定要完成的操作,操作數指參加運算的數據及其所在的單元地址。
虛擬機的字節碼指令亦是如此含義
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文件只會出現數字形式的操作碼
但是為了便於人識別,操作碼有他對應的助記符形式
接下來所有的指令的說明,都是以助記符形式表達的
但是要明確,實際的執行運行並不存在助記符這些東西,都是根據操作碼的值來執行
 
指令本身就是為了功能邏輯運算
運算自然要處理數據
所以說指令的設計是邏輯功能點與數據類型的結合
接下來先看下有哪些數據類型和邏輯功能點

數據類型

image_5b869c5d_c55
 
上一篇文章中已經說明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
 

按照邏輯功能進行划分

加載存儲指令

加載存儲指令用於局部變量與操作數棧交換數據
以及常量裝載到操作數棧
1、將一個局部變量加載到操作棧:
iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
操作數為局部變量的位置序號 序號從0開始 , 局部變量以slot為單位分配的
將序號為操作數的局部變量slot 的值 加載到操作數棧
指令可以讀作:將第(操作數+1)個 X(i l f d a)類型局部變量,推送至棧頂
ps: 操作數+1 是因為序號是從0開始的
 
2、將一個數值從操作數棧存儲到局部變量表:
istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
操作數為局部變量的位置序號 序號從0開始 , 局部變量以slot為單位分配的
將操作數棧的值保存到序號為操作數的局部變量slot中
指令可以讀作:將棧頂 X(i l f d a)類型的數值 保存到  第(操作數+1)個 局部變量中
 
3、將一個常量加載到操作數棧:
bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
操作數為將要操作的數值  或者常量池行號
指令可以讀作:將類型X的值xxx 推送至棧頂  或者是 將 行號為xxx的常量推送至棧頂
 
4、擴充局部變量表的訪問索引的指令:wide
 
形如  xxx_<n>以尖括號結尾的代表了一組指令 (例如 iload_<n>   代表了iload_0  iload_1  iload_2  iload_3)
這一組指令都是某個帶有一個操作數的通用指令(例如 iload)的特殊形式
對於這些特殊形式來說,他們表面上沒有操作數,但是操作數隱含在指令里面了,除此之外,語義與原指令並沒有任何的不同
(例如 iload_0  的語義與操作數為0時的iload 語義完全相同)
<>尖括號中的字母表示了指令隱含操作數的數據類型
<n>表示非負整數  <i>表示int    <l> 表示long <f> float  <d> double  而byte char short類型數據經常使用int來表示
下划線 _   的后面緊跟着的值就是操作數
需要注意的是 _<n> 的形式不是無限的,對於load 和 store系列指令
對於超過4個,也就是第5個,也就是下標是4 往后
都是直接只用原始形式 iload 4  不再使用_<n>的形式 所以你不會看到 load_4 load_5....或者store_4  store_5...

image_5b869c5d_3bf8
對於虛擬機執行方法來說,操作數棧是工作區, 所以數據的流向是對於他  操作數棧   來說的  
load就是局部變量數據加載到操作數棧 
store就是從操作數棧存儲到局部變量表
對於常量只有加載到操作數棧進行使用,沒有存儲的說法,他也比較特殊
 
對於上圖中的數據交換模型中,操作數棧是可以確定的也是唯一的,棧就在那里,不管你見或不見
對於操作數棧與局部變量交換數據時,需要確定的是  從 哪個局部變量取數據 或者保存到哪個局部變量中 
所以load 和 store的操作數都是局部變量的位置
 
對於操作數棧與常量交換數據,需要確定的是到底加載哪個值到操作數棧或者是從常量池哪行加載
所以加載常量到操作數棧的操作數 是 具體的數值 或者常量池行號
常量加載到操作數棧比較特殊單獨說明
他根據<數據類型>以及<數據的取值范圍>使用了不同的方式

const指令
該系列命令主要負責把簡單的數值類型送到棧頂
該系列命令不帶參數。只把簡單的數值類型送到棧頂時,才使用如下的命令。
比如對應int型該方式只能把-1,0,1,2,3,4,5(分別采用iconst_m1,iconst_0, iconst_1, iconst_2, iconst_3, iconst_4, iconst_5)
送到棧頂。對於int型,其他的數值請使用push系列命令(比如bipush)
指令碼    助記符                            說明
0x01        aconst_null                 將null推送至棧頂
0x02         iconst_m1                   將int型(-1)推送至棧頂
0x03         iconst_0                      將int型(0)推送至棧頂
0x04         iconst_1                      將int型(1)推送至棧頂
0x05         iconst_2                      將int型(2)推送至棧頂
0x06         iconst_3                      將int型(3)推送至棧頂
0x07         iconst_4                      將int型(4)推送至棧頂
0x08         iconst_5                      將int型(5)推送至棧頂
0x09         lconst_0                      將long型(0)推送至棧頂
0x0a         lconst_1                      將long型(1)推送至棧頂
0x0b         fconst_0                     將float型(0)推送至棧頂
0x0c         fconst_1                      將float型(1)推送至棧頂
0x0d         fconst_2                     將float型(2)推送至棧頂
0x0e         dconst_0                     將double型(0)推送至棧頂
0x0f         dconst_1                    將double型(1)推送至棧頂
簡言之 取值    -1~5 時,JVM采用const指令將常量壓入棧中

push指令
該系列命令負責把一個整型數字(長度比較小)送到到棧頂。
該系列命令有一個參數,用於指定要送到棧頂的數字。
注意該系列命令只能操作一定范圍內的整形數值,超出該范圍的使用將使用ldc命令系列。
指令碼        助記符                            說明
0x10          bipush    將單字節的常量值(-128~127)推送至棧頂
0x11           sipush    將一個短整型常量值(-32768~32767)推送至棧頂
 
ldc系列
該系列命令負責把數值常量或String常量值從常量池中推送至棧頂。
該命令后面需要給一個表示常量在常量池中位置(編號)的參數 也就是行號,
哪些常量是放在常量池呢?
比如:
final static int id=32768;   //32767+1 就不在sipush范圍內了
final static float double=8.8
對於const系列命令和push系列命令操作范圍之外的數值類型常量,都放在常量池中.
另外,所有不是通過new創建的String都是放在常量池中的
指令碼    助記符                               說明
0x12          ldc                   將int, float或String型常量值從常量池中推送至棧頂
0x13          ldc_w               將int, float或String型常量值從常量池中推送至棧頂(寬索引)
0x14          ldc2_w             將long或double型常量值從常量池中推送至棧頂(寬索引)
ps:所謂寬索引是指常量池行號 索引的字段長度, ldc 的索引只有8位  ldc_w的索引則有16位
對於寬索引,指令格式為 ldc_w ,indexbyte1,indexbyte2  會計算  (indexbyte1<<8) | indexbyte2 來生成一個指向當前常量池的無符號16位索引
說白了就是尋址長度
簡言之就是對於絕大多數的數值,都是存放在常量池中的 將需要使用ldc
對於一小部分可能比較常用的數值,則是可以直接把值當做操作數的 使用const 或者push
wide的含義   寬索引
字節碼的指令是單字節的,對於局部變量來說,最多容納256個局部變量
wide指令就是用於擴展局部變量數的 ,將8位的索引在擴展8位 也就是16位 最多65536
形式為 
wide 要被擴展的操作碼比如iload   操作數   (wide  iload 257 也就是  wide iload byte1  byte2)
iload操作碼是作為wide 操作碼的一個操作數來執行的
wide可以修飾 load  store  ret
如果wide修飾的是iinc 格式有些變化 
wide iinc  byte1 byte2 constbyte1 constbyte2  本身 iinc為 iinc  byte constbyte
擴展后的前兩個字節16位為局部變量索引
后兩個字節16位計算為 16位帶符號的增量
計算的形式依舊是  (constbyte1 << 8) | constbyte2
 
 

算數指令

運算后的結果自動入棧
運算或算術指令用於對兩個操作數棧上的值進行某種特定運算,並把結果重新存入到操作棧頂.
算術指令分為兩種:整型運算的指令和浮點型運算的指令.
無論是哪種算術指令,都使用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
 

操作數棧管理指令

操作數棧管理指令,顧名思義就是直接用於管理操作棧的
對於操作數棧的直接操作主要有 出棧/復制棧頂元素 / 以及 交換棧頂元素

出棧,   分為將操作數棧棧頂的幾個元素出棧,一個元素或者兩個元素
pop表示出棧, 數值代表個數
pop pop2

交換 將棧頂端的兩個數值進行交換
swap 
dup比較復雜一點
根本含義為復制棧頂的元素然后壓入棧
不過涉及到復制幾個元素,以及操作數棧的數據類型,所以比較復雜
 
上面提到過虛擬機處理的數據類型,有分類,分為1 和2兩種類型
虛擬機能處理的類型long和double為類型2 其余為類型1 也就是int returnAddress  reference等
 
dup      復制操作數棧棧頂一個元素  並且將這個值壓入到棧頂   value必須分類1
形式如下,右側為棧頂
... , value
... , value , value
 
dup_x1 復制操作數棧棧頂的一個元素.並插入到棧頂以下  兩個值之后   
形式如下,右側為棧頂,value1 插入到了第二個元素value2 下面  value1 和value2  必須分類1
... , value2, value1
... , value1, value2, value1
 
dup_x2 復制操作數棧棧頂的一個元素. 並插入棧頂以下 2 個 或 3個值之后
形式一 如果 value3, value2, value1  全都是分類1  使用此形式  插入棧頂三個值 以下 也就是value3之下
..., value3, value2, value1 →
..., value1, value3, value2, value1
 
形式二如果value1 是分類1   value2 是分類2  那么使用此形式 插入棧頂兩個值 以下,也就是value2 之下
..., value2, value1 →
..., value1, value2, value1
 
 
dup2  復制操作數棧棧頂一個或者兩個元素,並且按照原有順序,入棧到操作數棧
形式一 如果  value2, value1 全都是分類1  使用此形式 復制棧頂兩個元素,按照原順序,插入到棧頂
..., value2, value1 →
..., value2, value1, value2, value1
 
形式二 如果value 屬於分類2 使用此形式 復制棧頂一個元素,插入到棧頂
..., value →
..., value, value
 
dup2_x1復制操作數棧棧頂一個或者兩個元素,並且按照原有順序   插入棧頂以下  兩個或者三個 值  之后
形式一   如果  value3, value2, value1 都是分類1 使用此形式 復制兩個元素,插入棧頂下 三個值之后,也就是value3 之后
..., value3, value2, value1 →
..., value2, value1, value3, value2, value1
 
形式二 如果value1 是分類2 value2 是分類1 使用此形式   復制一個元素,插入到棧頂以下 兩個元素之后 
..., value2, value1 →
..., value1, value2, value1
 
 
dup_x2  復制操作數棧棧頂一個或者兩個元素,並且按照原有順序   插入棧頂以下  兩個或者三個 或者四個   值  之后
 
形式一   全都是分類1  使用此形式  復制兩個元素,插入到棧頂 第四個值后面
..., value4, value3, value2, value1 →
..., value2, value1, value4, value3, value2, value1
 
形式二 如果 value1 是分類2   value2 和 value3 是分類1 中的數據類型  使用此形式 復制一個元素 插入到棧頂 第三個值后面
..., value3, value2, value1 →
..., value1, value3, value2, value1
 
形式三 如果value 1  value2 是分類1   value3 是分類2 使用此形式 復制兩個元素 插入到棧頂 第三個值后面
..., value3, value2, value1 →
..., value2, value1, value3, value2, value1
 
形式四 當value1 和value2 都是分類2 使用此形式  復制一個元素 插入到棧頂 第二個值后面
..., value2, value1 →
..., value1, value2, value1
上面關於dup的描述摘自 虛擬機規范,很難理解
看起來是非常難以理解的,不妨換一個角度
我們知道局部變量的空間分配分為兩種long 和 double 占用2個slot  其他占用一個
操作數棧,每個單位可以表示虛擬機支持的任何的一個數據類型
不過操作數棧其實同局部變量一樣,他也是被組織一個數組, 每個元素的數據寬度和局部變量的寬度是一致的
所以對於long 和double占用2個單位長度  對於其他類型占用一個單位長度
雖然外部呈現上任何一個操作數棧可以表示任何一種數據類型,但是內部是有所區分的
如同局部變量表使用兩個單位存儲時,訪問元素使用兩個中索引小的那個類似的道理
所以可以把棧理解成線性的數組,
來一個long或者double 就分配兩個單位空間作為一個元素
其余類型就分配一個單位空間作為元素

既然棧本身的結構中,線性空間的最小單位的數據寬度同局部變量,
long和double占用兩個  也就是下面涉及說到的數據類型的分類1  和  分類2

假設棧的示意結構如下圖所示,(只是給出來一種可能每個元素的類型都可能是隨機的)
左邊表示呈現出來的棧元素 右邊是內部的線性形式  我們當做數組好了
image_5b869c5d_3c9b
對棧元素的處理,顯然指的是對於棧元素內部數組的處理
所以自然要分為    
到底是直接復制一個單位的數據        
還是直接復制兩個單位的數據 
 
 
一次復制占用一個單位空間   的指令 使用dup  
一次復制占用兩個單位空間   的指令 使用dup2
 
一次復制占用一個單位空間 時 假設復制的棧頂是array[0] 
dup 可以理解為dup_x0    
插入到他棧頂的內部線性結構的第(1+0)個元素下面 所以array[0] 對應的必然是一個完整的棧元素 ,必然是分類1 不可能是分類2的一半!
image_5b869c5d_24b7
dup_x1                           
插入到他棧頂的內部線性結構的第(1+1)個元素下面 也就是插到第二個下面  因為array[0] 對應value1為分類1  
如果接下來的是分類2的數據,必然接下來的兩個單元array[1] 和array[2]是不可分割的,也就是不可能插入到array[1] 后面,所以array[1] 對應value2 也必須是分類1 也就是兩個都是分類1
image_5b869c5d_5c27
 
dup_x2                          
插入到他棧頂的內部線性結構的第(1+2)個元素下面 也就是插到第三個后面,array[0] 對應value1為分類1 為分類1  
那么接下來的兩個單位array[1] 和array[2],可以是一個分類2  也可以是兩個分類1,都是可以的
image_5b869c5d_3651
 
image_5b869c5d_7212
 
一次復制占用兩個單位的數據類型 時
dup2 可以理解為dup2_x0   
插入到他棧頂的內部線性結構的第(2+0)個元素下面 
這一次復制的兩個單位array[0] 和 array[1],  到 array[1]下面  
可能是對應value1 和value2 表示兩個分類1  也可能是對應一個value1 表示類型為分類2 
image_5b869c5d_3101
image_5b869c5d_6ad0
 
dup2_x1   插入到他棧頂的內部線性結構的第(2+1)個元素下面 也就是復制array[0] 和 array[1] 到第三個元素 array[2]的下面
array[0] 和 array[1] 可能分別對應value1 和value2 表示兩個分類1 數據  也可能是對應着一個value1表示一個分類2數據
但是array[2] 作為第三個單位,既然能被分割,自然他必須是分類1
所以要么三個都是分類1,要么value1 分類2  value2 分類1
image_5b869c5d_3201
image_5b869c5d_3543


dup2_x2  插入到他棧頂的內部線性結構的第(2+2)個元素下面 也就是復制array[0] 和 array[1] 到第四個內部元素 array[3]的下面
一次復制兩個,放到第四個下面
這種情形下的組合就非常多了
全都是分類1的數據
image_5b869c5d_3cea

全部都是分類2
array[0]  和 array[1]  對應value1 表示一個分類2數據
array[2]  和 array[3]     對應value2 表示一個分類2數據
image_5b869c5d_5ddd

array[0]  和 array[1]  對應value1 表示一個分類2數據
array[2]  和 array[3]     對應value2 和 value3表示兩個分類1數據
image_5b869c5d_4ebd

array[0]  和 array[1]  對應value1 和value2 表示兩個分類1 數據
array[2]  和 array[3]    對應value3表示一個分類2數據
image_5b869c5d_4ea4

所以說只需要明確以下幾點,就不難理解dup指令
操作數棧指令操作的是棧內部的存儲單元,而不是以一個棧元素為單位的
long和double在棧元素內部需要兩個存儲單元,其余一個存儲單元
兩個相鄰的內部單位組合起來表示一個棧元素時,是不能拆分的

再回過頭看,所有的dup指令,不過是根據棧元素的實際存放的類型的排列組合,梳理出來的一些復制一個或者兩個棧頂元素的實際操作方式而已
就是因為他是逆向推導的,所以看起來不好理解
 

控制轉移指令

控制轉移指令可以讓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)

 
至此,虛擬機中的指令集的大致基本設計邏輯以及意圖已經基本介紹清楚了,如需要更深一步的了解,請查看虛擬機規范  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM