怎敢精——jvm指令
位元組碼是由操作碼opcode和運算元operand組成的。操作碼的長度是一個位元組-128到127
指令執行的虛擬碼如下:
do{
自動計算pc暫存器;
從PC暫存器的位置取出操作碼;
if(存在運算元) 取出運算元;
執行操作碼定義的操作
}while(處理下一次迴圈)
指令分類:
載入和儲存指令
算術指令
型別轉換指令
物件的建立和操作
運算元棧管理指令
控制轉移指令
方法呼叫和返回指令
丟擲異常
同步
大多數指令都不直接支援byte,short,char和boolean,處理時都說轉化為int型別
1、載入和儲存指令
格式為 _load_<n>,n表示運算元的個數是非負整數(lconst_<l>要求裡面存放long資料),下劃線上的內容是運算元型別
如iload_<2>。表示載入第二個變數,iload_<3>表示載入第三個變數 iload 5表示載入第五個變數
簡寫和對應具體關係如下:
i : int
l : long
f : float
d : double
c : char
b : byte
a : reference(引用)
本地變數載入到運算元棧指令:
iload,iload_<n>,lload,lload_<n>,fload,dload,aload
這裡沒有byte和char,iload和ilad_0是一樣的。其他類似的帶n的省略
從運算元棧儲存到區域性變量表指令:
istore,istore_<n>,lstore,fstore,dstore,astore
類似的省略
常量載入到運算元棧
bipush,sipush,ldc,ldc_w,ldc2_w,aonst_null,iconst_ml const_<i> lconst_<l>
上面帶n的是一類指令,如iload_<n>表示的是iload_<1>、iload_<2>、等等,但是lconst_<l>指載入long型常量到運算元棧。
載入和儲存是一個互逆的過程,載入是從變數到運算元棧,儲存是從運算元棧到變數
算術指令
算術指令是對兩個運算元棧上的值進行某種運算,並將結果重新壓入棧
大體分為:對整數計算和對浮點數計算
算術指令 | |
指令名 | 指令內容 |
加法指令 | iadd ladd dadd fadd |
減法指令 | sub lsub dsub fsub |
乘法指令 |
imul lmul dmul fmul |
除法指令 | idiv ldiv ddiv fdiv |
求餘指令 | irem lrem frem drem |
求負值指令 | ineg dneg lneg fneg |
移位指令 | ishl ishr ushr lshl lshr lushr |
按位或指令 | ior lor |
按位與指令 | iand land |
按位異或 | ixor lxor |
區域性變數自增指令 | iinc |
比較指令 | dcmpg dcmpl fcmpg fcmpl lcmp |
虛擬機器沒有明確規定整形資料溢位的情況。如列印1<<31只會列印負值,但是除以0會拋異常,浮點數除以0不會拋異常,但是會有丟擲一個字串
型別轉換指令
用於開發人員程式碼中的顯示型別轉換或者解決虛擬機器不完備的問題(byte,char和boolean的型別轉換成int)
Java虛擬機器規範SE8中原文提到,型別轉換指令其實並不存在,因為byte char和boolean本來就是按int儲存的所以不存在轉化,覺得這一段與一開始說的型別轉化用於虛擬機器不完備的情況,有點衝突。
型別轉換有兩種:寬化與窄化。
寬化是int轉long,轉double這種小資料向大資料轉化,窄化相反。
指令為i2l,i2f,i2d,l2f,l2d,f2d,2表示to 寬化轉換通常不會丟失精度
這裡因為long佔的空間比float要長,不明白為什麼是寬化轉換,跑了一下,可能是浮點數表示形式允許超過long的範圍
窄化就是l2i等,通常會丟失精度,丟失部分數值,但是不會丟擲異常
物件的建立與操作
建立類例項 |
new |
建立陣列 | newarray anewarray multianewayya |
訪問類欄位(靜態欄位)和類例項欄位(非靜態欄位) | getfield putfield getstatic putstatic |
將陣列載入到運算元棧 | baload caload saload iaload laload faload daload aaload |
將一個運算元棧的值儲存到陣列中 | 上面的load換成store |
取陣列長度指令 | arraylength |
檢查類例項或者陣列型別指令 | instanceof checkcast |
運算元棧管理指令
pop,pop2,dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2 swap
棧頂的一個或兩個元素出棧(兩個剛好用於計算我覺得) | pop pop2 |
複製棧頂一個或兩個數值並將複製值或雙份的複製值重新亞茹棧頂 | dup dup2 dup_x1 dup_x2 dup2_x1 dup2_x2 |
將棧頂兩個元素互換 | swap |
控制轉移指令
條件分支 | ifeq ifne iflt ifle ifgt ifge ifnull ifnonull if_icmpeq if_icmpne if_icmplt if_icmpgt if_icmpge if_acmpeq if_acmpne |
複合條件分支 | tableswitch lookupswitch |
無條件分支 | goto goto_w jsr jsr_w ret |
boolean byte char short都使用int型別的比較指令來完成,而對於long flaot double則先執行比較指令,返回一個整數數值到運算元棧中,隨後執行int的條件分支完成條件分支
所有的比較最終都會轉化為int的比較
方法呼叫和返回指令
invokevirtual指令用於呼叫物件的例項方法
invokeinterface用於呼叫介面,在執行時搜尋特定物件實現這個介面的方法並呼叫
invokespecial用於處理一些特殊例項方法,如初始化方法,私有方法和父類方法
invokestatic呼叫類方法(靜態方法)
invokedynamic用於在執行時動態解析出呼叫點限定符所引用的方法,並執行該方法,前面四條的指令分派固化在java虛擬機器內部,而這一條是由使用者的引導方法決定的
方法呼叫指令和資料型別無關,但是返回是資料相關的,如ireturn返回Boolean,char,char,short,int的值,還有lreturn,freturn,dreturn,areturn
還有一條用於返回void,例項化方法和初始化方法使用的return
異常處理指令
由athrow實現,虛擬機器規定了一些執行時異常在jvm檢測到異常時自動丟擲
同步指令
jvm可以支援方法級的同步和方法內部的一段指令序列同步,這兩種同步結構都是用同步鎖(monitor)來實現的。
方法級的同步鎖是隱式的,即無需通過位元組碼指令控制。
可以在方法常量池的方法表結構中的ACC_SYNCHRONNIZED檢視一個方法是否同步
當呼叫方法時,呼叫指令會先檢視是否有同步標記,如果有會讓執行執行緒先持有同步鎖,然後執行方法,執行完成後釋放鎖。
如果一個同步方法遇到異常且方法未對異常做處理,那麼會在異常丟擲的時候釋放鎖。
指令序列的同步通常用來表示java的synchronized塊,jvm的指令集中用monitorenter和monitorexit來支援這個關鍵字,一個進入鎖一個退出鎖。
結構化鎖定是指在方法呼叫期間每一個同步鎖退出都與前面同步鎖進入的情形相同,因為無法保證所有提交給jvm執行的程式碼都滿足結構化鎖定,因此jvm允許通過以下兩條規則使結構化鎖定成立
假設T是執行緒,M表示同步鎖
1、T在方法執行時持有同步鎖M的次數必須與T在方法執行時釋放同步鎖的次數相等
2、在方法呼叫過程中,任何時刻都不會出現執行緒T釋放同步鎖M的次數比T持有同步鎖M次數多的情況
對於指令的檢視可以使用 javap-c
public static void main(java.lang.String[]);
Code:
0: iconst_1 // 將常量載入入棧(數值是1-5的時候用iconst)
1: istore_1 // 讀取棧的資料儲存到變數
2: bipush 10 // 數值是-128~127的時候用bipush 壓入棧 如果arraylength
// 是5的話這裡就是iconst_5
4: newarray int // 建立一個數組
6: astore_2 // 將棧中的引用存入到變數中
7: return // 返回
1、前面說的一類 _load_<n>表示多個,但是這個應該是有上限的,如const只能載入1-5
2、超過一定個數就使用類似bipush這種指令了
3、javap 的第二列是表示操作碼對應的運算元型別