1. 程式人生 > >JVM學習筆記1:位元組碼指令集

JVM學習筆記1:位元組碼指令集

一.位元組碼指令集簡介:

Java虛擬機器的指令由一個位元組長度的、代表著某種特定操作含義的操作碼(opcode)以及跟隨其後的零至多個代表此操作所需引數的運算元(operand)所構成。虛擬機器中許多指令並不包含運算元,只有一個操作碼。

如果忽略異常處理,那麼java虛擬機器的直譯器通過下面這段虛擬碼的迴圈即可有效的工作。

do {
    自動計算pc暫存器以及從pc暫存器的位置取出操作碼;
    if (存在運算元) 取出運算元;
    執行操作碼所定義的操作;
} while (處理下一次迴圈)

二.位元組碼中的資料型別與java虛擬機器:

在Java虛擬機器指令集中:

  • 大多數的指令都包含了其所操作的資料型別資訊。(例如:iload指令是載入int型別的資料到運算元棧,fload則是載入float型別的資料。)
  • 它們的操作碼助記符中都有特殊字元來表明專門為哪種資料型別服務,例如: i 代表對int 型別資料操作、l 代表 long、s 代表 short、b 代表 byte、 c 代表 char、 f 代表 float、d 代表 double、a 代表 reference
  • 一些指令的助記符中無明確指令操作型別的字母,如arraylength。但運算元永遠只能是一個數組型別的物件。
  • 還有一些指令,如無條件跳轉指令goto則與資料型別無關。

由於java虛擬機器的操作碼長度只有1個位元組,故java虛擬機器的指令集對於特定的操作只提供了有限的型別相關指令,指令集將會故意設計成非完全獨立的(並非每種數型別和每一種操作都有有對應的指令

)。有些單獨的指令可以在必要的時候將一些不支援的型別轉換為可支援的型別

三.JVM指令集所支援的資料型別:

注意:大多數指令沒有支援整數型別 byte、char、short或boolean型別,是因為編譯器在編譯期或執行期將這些資料擴充套件為相應的int型別資料。因此,對於這些資料型別的操作,實際上是使用相應的int型別作為運算型別。

JVM實際型別與運算型別的對映關係表:

四.位元組碼操作指令:

1. 載入和儲存指令
(1)作用

載入和儲存指令用於將資料在棧幀中的區域性變量表和運算元棧之間來回傳輸。

(2)組成

這類指令包括如下內容:

將一個區域性變數載入到運算元棧: 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_m1, iconst_i, lconst_l, fconst_f, dconst_d;
擴充區域性變量表的訪問索引的指令: wide;
(3)注意

儲存資料的運算元棧和區域性變量表:主要就是由載入和儲存指令進行操作。除此之外,還有少量指令,如訪問物件的欄位或陣列元素的指令也會向運算元棧傳輸資料。
 

2. 運算指令
(1)作用

運算指令用於對兩個運算元棧上的值進行某種特定運算,並把結果重新存入到操作棧頂。

(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。
(3)運算時的溢位

資料運算可能會導致溢位,例如兩個很大的正整數相加,結果可能是一個負數。其實Java虛擬機器規範並無明確規定過整型資料溢位的具體結果,僅規定了在處理整型資料時,只有除法指令以及求餘指令中當出現除數為0時會導致虛擬機器丟擲異常ArithmeticException。

(4)運算模式

向最接近數舍入模式: jvm 要求在進行浮點數計算時, 所有的運算結果都必須舍入到適當的精度,非精確結果必須舍入為可被表示的最接近的精確值,如果有兩種可表示的形式與該值一樣接近,將優先選擇最低有效位為零的;
向零舍入模式:將浮點數轉換為整數時,採用該模式, 該模式將在目標數值型別中選擇一個最接近但是不大於原值的數字作為最精確的舍入結果;
(6)NaN值使用

當一個操作產生溢位時,將會使用有符號的無窮大表示,如果某個操作結果沒有明確的數學定義的話,將會使用 NaN值來表示。而且所有使用NaN值作為運算元的算術操作,結果都會返回 NaN;
 

3. 型別轉換指令
(1)作用

型別轉換指令可以將兩種不同的數值型別進行相互轉換,這些轉換操作一般用於實現使用者程式碼中的顯示型別轉換操作,或者用於處理位元組碼指令集中資料型別相關指令無法與資料一一對應的問題。

(2)寬化型轉換(Widening Numeric Conversions)

寬化型轉換:小範圍型別向大範圍型別的安全轉換。Java虛擬機器直接支援(即轉換時無需顯示的轉換指令)以下數值型別的轉換:

int到long, float或double;
long到float或double;
float到double;
(3)窄化型別轉換(Narrowing Numeric Conversion)

窄化型別轉換:必須顯示地使用轉換指令來完成,可能會導致轉換結果產生不同的正負號、不同數量級情況,會導致數值的精度丟失。

將 int 或long型別窄化轉換為整數型別 T 
轉換過程僅僅是丟棄除最低位N個位元組外的內容, N是型別T 的資料型別長度,這將可能導致轉換結果與輸入值有不同的正負號。(因為原來符號位處於數值的最高位,高位被丟棄後,轉換結果的符號就取決於低N個位元組的首位了)

將一個浮點值窄化轉換為整數型別 T(T限於int 或 long型別之一) 
在此轉換中遵循如下的轉換規則:

如果浮點值是NaN, 那轉換結果是int 或 long型別的0;
如果浮點值不是無窮大的話,浮點值使用 向零舍入模式取整,獲得整數值v,且v在目標型別T(int或double)的表示範圍內;
否則,根據v的符號,轉換為T所能表示的最大或最小整數;
將一個double 型別窄化轉換為 float型別 
通過向最接近數舍入模式舍入一個可以使用float型別表示的數字。最後結果根據下面這3條規則判斷:

如果轉換結果的絕對值太小而無法使用 float來表示,將返回 float型別的正負零。
如果轉換結果的絕對值太大而無法使用 float來表示,將返回 float型別的正負無窮大。
對於double 型別的 NaN值將按規定轉換為 float型別的 NaN值。
(4)程式碼實踐
 

public long convert()
    {
        short shortNum = 50;
        int intNum = 1000;
        long result = shortNum  * intNum  + 1000000;
        return result;
    }

編譯後,生成的位元組碼序列:

public long convert();
Code:
Stack=2, Locals=5, Args_size=1 //聲明瞭棧的最大深度、本地字數和傳入引數數,對於物件方法,會傳入this引用,因此這裡Arg_szie=1,如上的程式,this會佔用1個 字,shortNum 和 intNum分別佔1個字,result佔2個字(long),因此這裡Locals=5

0: bipush 50 //將50入到棧,在棧中會佔1個字的位置
2: istore_1 //將棧頂值彈出設給第2個本地變數(傳入引數也會以本地變數的方式存在,在這了第1個引數是this),這兩段指令等價於short shortNum  = 80,從這裡可以看出,JVM直接把short當做integer來運算的
3: sipush 1000 //與上類似,把1000入到棧頂,這裡1000超過了b所能表示的範圍,所以是sipush
6: istore_2 //同樣的,把堆疊值彈出並設給第3個本地變數,這兩段等價於int  intNum = 1000
7: iload_1 
8: iload_2 //把第2個本地變數(shortNum 和 intNum)入棧
9: imul //乘運算,彈出2個棧頂值(shortNum 和 intNum),並把運算結果入棧,這時候棧頂值就是 shortNum *  intNum
10: ldc #16; //1000000超過short能夠表示的範圍,會以常量池中條目的形式存在,這裡#16就是1000000,這裡把1000000入棧
12: iadd //彈出棧頂值2個字的值,並進行add操作,把add結果再入棧,這時shortNum * intNum和1000000被彈出棧,並把 shortNum * intNum+1000000的值入棧
13: i2l //從棧頂彈出1個字的值,並轉換成l型,再入到棧中(這時候,shortNum * intNum  +1000000會佔用棧頂2個字的位置。
14: lstore_3 //從棧頂彈出2個字(因為是l型的),並把結果賦給第4和第5個local位置(l需要佔2個位置),想當於把運算結果賦給result
15: lload_3 //將第4和第5個local位置的值入棧
16: lreturn //返回指令,將棧頂2個位置的值彈出,並壓入方法呼叫者的操作棧(上一個方法的操作棧),同時把本方法的操作棧清空

4. 物件建立與訪問指令
(1)作用

雖然類例項和陣列都是物件,但Java虛擬機器對它們的建立與操作使用了不同的位元組碼指令。物件建立後,就可以通過物件訪問指令獲取物件例項或陣列例項中的欄位或者陣列元素。

(2)組成

這類指令包括如下內容:

建立類例項的指令: new;
建立陣列的指令: newarray, anewarray, multianewarray;
訪問類欄位(static欄位或稱為類變數)和例項欄位的指令: getfield, putfield, getstatic, putstatic;
把一個數組元素載入到運算元棧的指令: baload, caload, saload, iaload, laload, faload, daload, aaload;
將一個運算元棧的值儲存到陣列元素中的指令: bastore, castore, sastore, iastore, fastore, dastore, aastore;
取陣列長度指令: arrayLength;
檢查類例項型別的指令: instanceof, checkcast;
(3)程式碼實踐
 

public void newarray()
    {
        //單維陣列
        int[] iarray = new int[10];
        iarray[3] = 10;
        int length = iarray.length;
        int result = iarray[3];

        //物件陣列
        Object[] objs = new Object[10];
    }

編譯後,生成的位元組碼序列:

public void newarray();
Code:
Stack=3, Locals=6, Args_size=1
0: bipush 10 //將陣列長度入棧
2: newarray int //建立int[10],並將陣列引用入棧
4: astore_1 //將建立的陣列的引用出棧,賦給第2個本地變數,即iarray
5: aload_1 //將iarray入棧
6: iconst_3 //陣列下標是3
7: bipush 10 //值是10
9: iastore //設定iarray[3] = 10,並將3個值出棧
10: aload_1 //將iarray入棧
11: arraylength //將iarray出棧,獲得陣列長度,並將長度值入棧
12: istore_2 //將陣列長度值出棧,並賦給第3個本地變數,即length
13: aload_1 //將iarray入棧
14: iconst_3 //陣列下標是3
15: iaload //將如上2個引數出棧,並將iarray[3]的值入對棧
16: istore_3 //將棧頂值(即iarray[3])出棧,並賦給第4個本地變數,即使result
17: bipush 10
19: anewarray #3; //class java/lang/Object,建立Object陣列
22: astore 4
24: return

5. 運算元棧管理指令
(1)作用

如同操作一個普通資料結構中的堆疊那樣,jvm提供的運算元棧管理指令,可以用於直接操作運算元棧的指令

(2)組成

這類指令包括如下內容:

將一個或兩個元素出棧: pop,pop2;
複製棧頂一個或兩個數值並將複製值或雙份的複製值重新壓入棧頂: dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2;
將棧最頂端的兩個數值交換: swap;
 

6. 控制轉移指令
(1)作用

控制轉移指令 可以讓Java虛擬機器有條件或無條件地從指定的位置指令而不是控制轉移指定的下一條指令繼續執行程式。從概念模型上理解,可以認為控制轉移指令就是在有條件或無條件地修改PC暫存器的值。

(2)組成

這類指令包括如下內容:

條件分支: ifeq, iflt, ifle, ifne, ifgt, ifge, 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;
(3)注意

與前面運算規則一致:

對於boolean、byte、char、short型別的條件分支比較操作,都是使用int型別的比較指令完成;
對於long、float、double型別的條件分支比較操作,則會先執行相應型別的比較運算指令,運算指令會返回一個整型值到運算元棧中,隨後再執行 int 型別的條件分支比較操作來完成整個分支跳轉。
由於各型別的比較最終都會轉為 int 型別的比較操作,所以Java虛擬機器提供的 int 型別的條件分支指令是最為豐富和強大的。

(4)程式碼例項
 

public int ifAndSwitch(int i)
    {
        if (i > 100)
        {
            return 200;
        }

        //case語句比較連續,會翻譯成tableswitch
        switch (i)
        {
        case 1:
            return 1;
        case 2:
            return 2;
        }

        //case語句不連續,會翻譯成lookupswitch
        switch (i)
        {
        case 1:
            return 1;
        case 100:
            return 100;
        }

        return 0;
    }

編譯後,生成的位元組碼序列:

public int ifAndSwitch(int);
Code:
Stack=2, Locals=2, Args_size=2
0: iload_1 //將第2個引數入棧,即i
1: bipush 100 //將100入棧
3: if_icmple 10 //如果i<=100,則跳轉到第10條語句
6: sipush 200 
9: ireturn //返回200
10: iload_1 //將第2個引數入棧,即i
11: tableswitch{ //1 to 2
1: 32;
2: 34;
default: 36 }
//case語句比較連續,使用tableswitch
32: iconst_1
33: ireturn
34: iconst_2
35: ireturn
36: iload_1
37: lookupswitch{ //2
1: 64;
100: 66;
default: 69 }
//case語句不連續,使用lookupswitch
64: iconst_1
65: ireturn
66: bipush 100
68: ireturn
69: iconst_0
70: ireturn

7. 方法呼叫和返回指令
(1)組成

具體作用在後續“虛擬機器執行位元組碼引擎”時再講解,這裡僅作了解即可。這類指令包括如下內容:

invokevirtual:用於呼叫物件的例項方法, 根據物件的實際型別進行分派(虛方法分派),這也是java中最常見的方法分派方式;
invokeinterface:用於呼叫介面方法, 它會在執行時搜尋一個實現了這個介面方法的物件,找出合適的方法進行呼叫;
invokespecial:用於呼叫一些需要特殊處理的例項方法, 包括例項初始化方法,私有方法和父類方法;
invokestatic:用於呼叫類方法(static方法);
invokedynamic:用於在執行時動態解析出呼叫點限定符所引用的方法,並執行該方法,前面4條呼叫指令的分派邏輯都固化在 java 虛擬機器內部,而 invokedynamic指令的分派邏輯是由使用者所設定的引導方法決定的;
(2)注意

方法呼叫指令與資料型別無關,而方法返回指令是根據返回值的型別區分的,包括ireturn(當返回值是 boolean、byte、char、short和int 型別時使用)、lreturn、freturn、dreturn和areturn,另外還有一條return 指令供宣告為 void的方法、例項初始化方法以及類和介面的類初始化方法使用。
 

8. 異常處理指令
(1)athrow指令

在Java程式中顯示丟擲異常的操作(throw語句)都是由athrow指令來實現。

除了使用throw語句顯示丟擲異常情況之外,JVM規範還規定了許多執行時異常會在其他Java虛擬機器指令檢測到異常狀況時自動丟擲。例如,在之前介紹的整數運算時,當除數為零時,虛擬機器會在 ididv或 ldiv指令中丟擲 ArithmeticException異常。

(2)注意

在Java虛擬機器中,處理異常(catch語句)不是由位元組碼指令來實現的(早期使用jsr、ret指令),而是採用異常表來完成的。
 

9. 同步指令
(1)組成

java虛擬機器支援兩種同步結構:方法級的同步 和 方法內部一段指令序列的同步,這兩種同步都是使用管程(monitor)來支援的。

方法級的同步:是隱式的, 即無須通過位元組碼指令來控制,它實現在方法呼叫和返回操作之中。虛擬機器可以從方法常量池的方法表結構中的 ACC_SYNCHRONIZED 訪問標誌得知一個方法是否宣告為同步方法;

同步一段指令集序列:通常是由java 中的synchronized語句塊來表示的,jvm的指令集有 monitorenter 和 monitorexit 兩條指令來支援 synchronized關鍵字的語義。

(2)synchronized 測試

下面根據一段簡單的程式碼來測試方法內部一段指令序列的同步,理解若要正確實現synchronized 關鍵字,需要Javac 編譯器與JVM兩者共同協作支援,程式碼如下:
 

private int age;
    public void synchronizedTest()
    {
        Object obj = new Object();
        synchronized (obj)
        {
            int result = age;       
        }
    }

編譯後,生成的位元組碼序列:

(3)測試分析

編譯器必須確保無論方法通過何種方式完成,方法中呼叫過的每條 monitorenter指令都必須執行其對應的 monitorexit指令,而無論這個方法是正常結束還是 異常結束。

從位元組碼序列中可以看出,為了保證在方法異常完成時 monitorenter和monitorexit指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,它可處理所有的異常,目的是用來執行monitorexit指令。
--------------------- 
作者:lemonGuo 
來源:CSDN 
原文:https://blog.csdn.net/ITermeng/article/details/75373436 
版權宣告:本文為博主原創文章,轉載請附上博文連結!