位元組碼基礎

LZC發表於2020-08-21

虛擬機器棧和棧幀

Hotspot JVM是一個基於棧的虛擬機器,每個執行緒都有一個虛擬機器棧用來儲存棧幀,每次方法呼叫都伴隨著棧幀的建立、銷燬。Java虛擬機器棧的釋義如圖所示

當執行緒請求分配的棧容量超過Java虛擬機器棧允許的最大容量時,Java虛擬機器將會丟擲StackOverflowError異常,可以用JVM命令列引數 -Xss來指定執行緒棧的大小,比如 -Xss:256k用於將棧的大小設定為256KB。

每個執行緒都擁有自己的Java虛擬機器棧,一個多執行緒的應用會擁有多個Java虛擬機器棧,每個棧擁有自己的棧幀。

棧幀是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,隨著方法呼叫而建立,隨著方法結束而銷燬。棧幀的儲存空間分配在Java虛擬機器棧中,每個棧幀擁有自己的區域性變數表(LocalVariable)、運算元棧(Operand Stack)和指向常量池的引用,如圖所示。

區域性變數表

每個棧幀內部都包含一組稱為區域性變數表的變數列表,區域性變數表的大小在編譯期間就已經確定,對應class檔案中方法Code屬性的locals欄位,Java虛擬機器會根據locals欄位來分配方法執行過程中需要分配的最大的區域性變數表容量。程式碼示例如下。

public class T {
    public int addFun(int a, int b) {
        return a+b;
    }
}

使用javac -g:vars T.java進行編譯,然後執行javap -v -l T.class檢視位元組碼

public com.example.demo.test.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/example/demo/test/T;

  public int addFun(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lcom/example/demo/test/T;
            0       4     1     a   I
            0       4     2     b   I

可以發現,預設情況,JVM會給我們生成一個預設的無慘建構函式。檢視每個方法的LocalVariableTable(區域性變數表)可知,當一個例項方法(非靜態方法)被呼叫時,第0個區域性變數是呼叫這個例項方法的物件的引用,也就是我們所說的this。

運算元棧

每個棧幀內部都包含一個稱為運算元棧的後進先出(LIFO)棧,棧的大小同樣也是在編譯期間確定。Java虛擬機器提供的很多位元組碼指令用於從區域性變數表或者物件例項的欄位中複製常量或者變數到運算元棧,也有一些指令用於從運算元棧取走資料、運算元據和把操作結果重新入棧。在方法呼叫時,運算元棧也用於準備呼叫方法的引數和接收方法返回的結果

比如iadd指令用於將兩個int型的數值相加,它要求執行之前運算元棧已經存在兩個int型數值,在iadd指令執行時,兩個int型數值從運算元棧中出棧,相加求和,然後將求和的結果重新入棧。1 + 2對應的指令執行過程,如圖所示。

整個JVM指令執行的過程就是區域性變數表與運算元棧之間不斷載入、儲存的過程,如圖所示。

那麼,如何計算運算元棧的最大值?運算元棧容量最大值對應方法Code屬性的stack,表示當前方法的運算元棧在執行過程中任何時間點的最大深度。呼叫一個成員方法會將this和所有引數入棧,呼叫完畢this和引數都會出棧。如果方法有返回值,會將返回值入棧。

public class T {
    public void demo() {
        addFun(123,456);
    }
    public int addFun(int a, int b) {
        return a+b;
    }
}

demo方法的stack等於3,因為呼叫addFun方法會將this、123、456這三個變數壓棧到棧上,棧的深度為3,呼叫完後全部出棧。

位元組碼如下所示

public void demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: bipush        123
         3: sipush        456
         6: invokevirtual #2                  // Method addFun:(II)I
         9: pop
        10: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/example/demo/test/T;

  public int addFun(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lcom/example/demo/test/T;
            0       4     1     a   I
            0       4     2     b   I

位元組碼指令

載入和儲存指令

載入(load)和儲存(store)相關的指令是使用得最頻繁的指令,分為load類、store類、常量載入這三種。

1)load類指令是將區域性變數表中的變數載入到運算元棧,比如iload_0將區域性變數表中下標為0的int型變數載入到運算元棧上,根據不同的資料變數型別還有lload、fload、dload、aload這些指令,分別表示載入區域性變數表中long、float、double、引用型別的變數。

2)store類指令是將棧頂的資料儲存到區域性變數表中,比如istore_0將運算元棧頂的元素儲存到區域性變數表中下標為0的位置,這個位置的元素型別為int,根據不同的資料變數型別還有lstore、fstore、dstore、astore這些指令。

3)常量載入相關的指令,常見的有const類、push類、ldc類。const、push類指令是將常量值直接載入到運算元棧頂,比如iconst_0是將整數0載入到運算元棧上,bipush 100是將int型常量100載入到運算元棧上。ldc指令是從常量池載入對應的常量到運算元棧頂,比如ldc #10是將常量池中下標為10的常量資料載入到運算元棧上。

為什麼同是int型常量,載入需要分這麼多型別呢?這是為了使位元組碼更加緊湊,int型常量值根據值 n 的範圍,使用的指令按照如下的規則。

❏ 若n在[-1, 5] 範圍內,使用iconst_n的方式,運算元和操作碼加一起只佔一個位元組。比如iconst_2對應的十六進位制為0x05。-1比較特殊,對應的指令是iconst_m1(0x02)。

❏ 若n在[-128, 127] 範圍內,使用bipush n的方式,運算元和操作碼一起只佔兩個位元組。比如 n 值為100(0x64)時,bipush 100對應十六進位制為0 x1064。

❏ 若n在[-32768, 32767] 範圍內,使用sipush n的方式,運算元和操作碼一起只佔三個位元組,比如 n 值為1024(0x0400)時,對應的位元組碼為sipush 1024(0x110400)。

❏ 若n在其他範圍內,則使用ldc的方式,這個範圍的整數值被放在常量池中,比如 n值為40000時,40000被儲存到常量池中,載入的指令為ldc #i, i為常量池的索引值。完整的載入儲存指令見表所示。

位元組碼指令的別名很多是使用簡寫的方式,比如ldc是loadconstant的簡寫,bipush對應byte immediate push, sipush對應short immediate push。

運算元棧指令

常見的運算元棧指令有pop、dupswap

pop指令用於將棧頂的值出棧,一個常見的場景是呼叫了有返回值的方法,但是沒有使用這個返回值,比如下面的demo方法。

public class T {
    public void demo() {
        addFun(123,"456");
    }
    public String addFun(int a, String b) {
        return a+b;
    }
}

demo方法對應的位元組碼如下所示

0: aload_0
1: bipush        123
3: ldc           #2                  // String 456
5: invokevirtual #3                  // Method addFun:(ILjava/lang/String;)Ljava/lang/String;
8: pop
9: return

第8行有一個pop指令用於彈出呼叫addFun方法的返回值。

dup指令用來複制棧頂的元素並壓入棧頂,後面講到建立物件的時候會用到dup指令。

swap用於交換棧頂的兩個元素,如圖所示。

還有幾個稍微複雜一點的棧操作指令:dup_x1、dup2_x1和dup2_x2。下面以dup_x1為例來講解。dup_x1是複製運算元棧棧頂的值,並插入棧頂以下2個值,看起來很繞,把它拆開來看其實分為了五步,如圖所示。

v1 = stack.pop(); // 彈出棧頂的元素,記為v1
v2 = stack.pop(); // 再次彈出棧頂的元素,記為v2
state.push(v1);   // 將v1 入棧
state.push(v2);   // 將v2 入棧
state.push(v1);   // 再次將v1 入棧

接下來看一個dup_x1指令的實際例子,程式碼如下。

class Hello {
    private int id;
    public int incAndGetId() {
        return ++id;
    }
}

incAndGetId方法對應的位元組碼如下。

Code:
  stack=3, locals=1, args_size=1
     0: aload_0
     1: dup
     2: getfield      #2                  // Field id:I
     5: iconst_1
     6: iadd
     7: dup_x1
     8: putfield      #2                  // Field id:I
    11: ireturn
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0      12     0  this   Lcom/example/demo/test/Hello;

假如id的初始值為42,呼叫incAndGetId方法執行過程中運算元棧的變化如圖所示。

第0行:aload_0將this載入到運算元棧上。

第1行:dup指令將複製棧頂的this,現在運算元棧上有兩個this,棧上的元素是 [this, this]。

第2行:getfield #2指令將42載入到棧上,同時將一個this出棧,棧上的元素變為[this, 42]。

第5行:iconst_1將常量1載入到棧上,棧中元素變為[this, 42, 1]。

第6行:iadd將棧頂的兩個值出棧相加,並將結果43放回棧上,現在棧中的元素是[this, 43]。

第7行:dup_x1將棧頂的元素43插入this之下,棧中元素變為[43, this, 43]。

第8行:putfield #2將棧頂的兩個元素this和43出棧,現在棧中元素只剩下棧頂的[43],最後的ireturn指令將棧頂的43出棧返回。完整的運算元棧指令介紹如表所示。

運算和型別轉換指令

Java中有加減乘除等相關的語法,針對位元組碼也有對應的運算指令,如表所示。

控制轉移指令

以下面程式碼中的isPositive方法為例,它的作用是判斷一個整數是否為正數。

public int isPositive(int n) {
    if (n > 0 ) {
        return 1;
    } else {
        return 0;
    }
}

對應的位元組碼如下所示

Code:
  stack=1, locals=2, args_size=2
     0: iload_1
     1: ifle          6
     4: iconst_1
     5: ireturn
     6: iconst_0
     7: ireturn
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       8     0  this   Lcom/example/demo/test/T;
        0       8     1     n   I
  StackMapTable: number_of_entries = 1
    frame_type = 6 /* same */

第0行:將區域性變數表中下標為1的整形變數載入到運算元棧上,也就是載入引數n

第1行:ifle指令的作用是將運算元棧頂元素出棧跟0進行比較,如果小於等於0則跳轉到特定的位元組碼處,如果大於0則繼續執行接下來的位元組碼。如果棧頂元素小於等於0,則跳轉到第6行。

第4行:把常量1載入到運算元棧上

第5行:將棧頂的整數出棧,方法呼叫結束

第6行:把常量0載入到運算元棧上

第7行:將棧頂的整數出棧,方法呼叫結束

for語句的位元組碼原理

以sum相加求和的例子來看for迴圈的實現細節

public class T {
    public int sum(int[] numbers) {
        int sum = 0;
        for (int i = 0; i < numbers.length; i++) {
            sum = sum + i;
        }
        return sum;
    }
}

位元組碼如下

Code:
  stack=3, locals=4, args_size=2
     /* 將常量0入棧,棧結構[0]*/
     0: iconst_0
     /* 棧頂元素出棧並儲存到下標為2的區域性變數sum中,棧結構[]*/
     1: istore_2
     /* 將常量0入棧,棧結構[0]*/
     2: iconst_0
     /* 棧頂元素出棧並儲存到下標為3的區域性變數i中,棧結構[]*/
     3: istore_3
     /* 下標為3的區域性變數i入棧,棧結構[i] */
     4: iload_3
     /* 下標為1的區域性變數陣列numbers入棧,棧結構[numbers,i] */
     5: aload_1
     /* 陣列出棧,獲取陣列長度儲存到棧頂,假設陣列長度為n,棧結構[n,i] */
     6: arraylength
     /* 棧頂元素出棧n,棧頂元素出棧i。若 i >= n則跳轉到22行,否則繼續執行 */
     7: if_icmpge     22
     /* 下標為2的區域性變數sum入棧,棧結構[sum] */        
    10: iload_2
     /* 下標為1的區域性變數陣列numbers入棧,棧結構[numbers,sum] */
    11: aload_1
    /* 下標為3的區域性變數陣列i入棧,棧結構[i,numbers,sum] */
    12: iload_3
    /* i出棧、numbers出棧,把下標為i的陣列元素載入到運算元棧上,假設number[i] = x, 棧結構[x,sum]*/
    13: iaload
    /* x出棧,sum出棧,將x與sum相加的結果Y載入到運算元棧上,棧結構[Y] */
    14: iadd
    /* Y出棧儲存到本地變數表中下標為2的sum中 */
    15: istore_2
    /* iinc是直接對區域性變數進行自增 */
    /* 這裡是對區域性變數下標為3的變數i進行自增操作並將結果儲存到區域性變數表裡面 */
    16: iinc          3, 1
    /* 跳轉到第四行 */    
    19: goto          4
    /* 下標為2的區域性變數sum入棧 */
    22: iload_2
    /* 將棧頂的整數出棧,方法呼叫結束 */
    23: ireturn
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        4      18     3     i   I
        0      24     0  this   Lcom/example/demo/test/T;
        0      24     1 numbers   [I
        2      22     2   sum   I
  StackMapTable: number_of_entries = 2
    frame_type = 253 /* append */
      offset_delta = 4
      locals = [ int, int ]
    frame_type = 250 /* chop */
      offset_delta = 17

i++與++i原理

i++原理

public class T {
    public static void main(String[] args) {
        int a = 10;
        int b = a++;
        System.out.println(b);
    }
}

執行上述程式碼輸出結果是10,而不是11。上面程式碼對應的位元組碼如下所示

Code:
  stack=2, locals=3, args_size=1
     // 將整數10載入到運算元棧上,運算元棧[10] 
     0: bipush        10
     // 出棧,將結果儲存到下標為1的區域性變數a中,運算元棧[]    
     2: istore_1
     // 下標為1的區域性變數a入棧,運算元棧[x1],此時的x1=a=10
     3: iload_1
     // iinc indexbyte,constbyte
     // 將整數值constbyte加到下標為indexbyte的int型別的區域性變數中。
     // 將本地變數a的值加1,此時a=11
     // 該操作是在本地變數表中執行的,所以此時的運算元棧為[10],而本地變數表裡面的a=11
     4: iinc          1, 1
     // 出棧,將結果載入到下表為2的本地變數b中,所以此時的b=10    
     7: istore_2
     // getstatic、invokevirtual指令後面再分析
     8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    11: iload_2
    12: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
    15: return

區域性變數表如下

Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
3 13 1 a I
8 8 2 b I

++i原理

public class T {
    public static void main(String[] args) {
        int a = 10;
        int b = ++a;
        System.out.println(b);
    }
}

執行上述程式碼輸出結果是11。上面程式碼對應的位元組碼如下所示

Code:
  stack=2, locals=3, args_size=1
     // 將整數10載入到運算元棧上,運算元棧[10]  
     0: bipush        10
     // 出棧,將結果儲存到下標為1的區域性變數a中,運算元棧[]        
     2: istore_1
     // iinc indexbyte,constbyte
     // 將整數值constbyte加到下標為indexbyte的int型別的區域性變數中。
     // 將本地變數a的值加1,此時a=11
     // 該操作是在本地變數表中執行的
     3: iinc          1, 1
     // 下標為1的區域性變數a入棧,運算元棧[x1],此時的x1=a=11    
     6: iload_1
     // 出棧,將結果儲存到下標為2的本地變數b中,所以此時的b=11
     7: istore_2
     8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    11: iload_2
    12: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
    15: return

區域性變數表如下

Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
3 13 1 a I
8 8 2 b I

try-catch位元組碼分析

public class T {
    public int fun(int n) {
        try {
            if (n > 10) {
                throw new Exception("n > 10");
            }
        } catch (Exception e) {
            System.out.println("異常");
        }
        return n;
    }
}

上述程式碼對應的位元組碼

Code:
  stack=3, locals=3, args_size=2
     // 將下標為1的本地變數n入棧,運算元棧[n] 
     0: iload_1
     // 將整數10入棧,運算元棧[10,n] 
     1: bipush        10
     // 10出棧,n出棧,如果 n <= 10 則跳轉到 16 行    
     3: if_icmple     16
     //     
     // 建立了一個Exception例項引用,假設為 x,將這個引用壓入運算元棧頂, 運算元棧[x] 
     // 此時還沒有呼叫初始化方法,    
     6: new           #2                  // class java/lang/Exception
     // 複製棧頂的元素並壓入棧頂, 運算元棧[x,x]  
     9: dup
     // 從常量池載入對應的常量到運算元棧頂,["n > 10",x,x] 
    10: ldc           #3                  // String n > 10
    // 出棧"n > 10",出棧x,執行構造方法Exception(String message),運算元棧[x]  
    12: invokespecial #4                  // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
    // 將棧頂的異常丟擲
    15: athrow
    // 如果不丟擲異常就會跳轉到28行
    // 如果有異常,則檢視Exception table
    // 如果丟擲了異常型別為type的異常,就會跳轉到target指標表示的位元組碼處繼續執行
    16: goto          28
    // 將接收到的異常儲存到下標為2的區域性變數e中    
    19: astore_2
    // 呼叫System.out獲取PrintStream併入棧
    20: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
    // 將
    23: ldc           #6                  // String 異常
    25: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    // 將下標為1的本地變數n入棧,運算元棧[n] 
    28: iload_1
    // 將棧頂的整數出棧並返回
    29: ireturn
  Exception table:
     from    to  target type
         0    16    19   Class java/lang/Exception
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
       20       8     2     e   Ljava/lang/Exception;
        0      30     0  this   Lcom/example/demo/test/T;
        0      30     1     n   I
  StackMapTable: number_of_entries = 3
    frame_type = 16 /* same */
    frame_type = 66 /* same_locals_1_stack_item */
      stack = [ class java/lang/Exception ]
    frame_type = 8 /* same */

finally位元組碼分析

finally語句塊保證一定會執行,以下面的fun方法為例,如果n=10,但是最終返回的結果還是10而不是10。

public class T {
    public int fun(int n) {
        try {
            return n;
        }  finally {
            n = n+10;
        }
    }
}

上述程式碼對應的位元組碼如下

Code:
  stack=2, locals=4, args_size=2
     // 將下標為1的區域性變數n入棧,運算元棧[n] 
     0: iload_1
     // 出棧並儲存到下標為2的區域性變數中,運算元棧[] 
     1: istore_2
     // 檢視Exception table可以發現,如果這裡接收到異常,那麼將調轉到第9行
     // 將下標為1的區域性變數n入棧,運算元棧[n] 
     2: iload_1
     // 將整數10入棧,運算元棧[10,n] 
     3: bipush        10
     // 棧頂出棧兩個元素出棧並相加,相加結果入棧,[10+n]    
     5: iadd
     // 棧頂元素出棧儲存到下標為1的本地變數n中
     6: istore_1
     // 將下標為2的區域性變數入棧,運算元棧[n]  
     7: iload_2
     // 棧頂整形元素出棧並返回
     8: ireturn
     // 執行到這裡說明程式丟擲了異常
     // 出棧並儲存到下標為3的區域性變數中,運算元棧[] 
     9: astore_3
     // 將下標為1的區域性變數n入棧,運算元棧[n] 
    10: iload_1
    // 將整數10入棧,運算元棧[10,n]
    11: bipush        10
    // 棧頂出棧兩個元素出棧並相加,相加結果入棧,[10+n]        
    13: iadd
    // 棧頂元素出棧儲存到下標為1的本地變數n中
    14: istore_1
    // 將下標為3的區域性變數入棧,運算元棧[n]  
    15: aload_3
    // 這裡需要將異常丟擲
    16: athrow
  Exception table:
     from    to  target type
         0     2     9   any
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0      17     0  this   Lcom/example/demo/test/T;
        0      17     1     n   I
  StackMapTable: number_of_entries = 1
    frame_type = 73 /* same_locals_1_stack_item */
      stack = [ class java/lang/Throwable ]

從上面的分析可以知道,在執行return n;之前,會把n儲存在一個臨時變數裡面,假設為X,然後執行n=n+10;,最後返回的確是臨時變數X的值。

物件相關的位元組碼指令

public class T {
    public int a = 10;
    public static void main(String[] args) {
        T t = new T();
    }
}

對應的位元組碼如下所示

Code:
  stack=2, locals=2, args_size=1
     0: new           #3                  // class com/example/demo/test/T
     3: dup
     4: invokespecial #4                  // Method "<init>":()V
     7: astore_1
     8: return
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       9     0  args   [Ljava/lang/String;
        8       1     1     t   Lcom/example/demo/test/T;

一個物件建立需要三條指令,new、dup、<init> 方法的invokespecial呼叫。在JVM中,類的例項初始化方法是<init>,呼叫new指令時,只是建立了一個類例項引用,將這個引用壓入運算元棧頂,此時還沒有呼叫初始化方法。

<init> 方法是物件初始化方法,類的構造方法非靜態變數的初始化物件初始化程式碼塊都會被編譯進這個方法中。

使用invokespecial呼叫<init> 方法後才真正呼叫了構造器方法,那中間的dup指令的作用是什麼?

invokespecial會消耗運算元棧頂的類例項引用,如果想要在invokespecial呼叫以後棧頂還有指向新建類物件例項的引用,就需要在呼叫invokespecial之前複製一份類物件例項的引用,否則呼叫完<init> 方法以後,類例項引用出棧以後,就再也找不回剛剛建立的物件引用了。有了棧頂的新建物件的引用,就可以使用astore指令將物件引用儲存到區域性變數表

synchronized位元組碼分析

public class T {
    private int count = 0;
    public void increase() {
        synchronized (this) {
            count++;
        }
    }
}

位元組碼

Code:
  stack=3, locals=3, args_size=1
     // 將this物件引用入棧,運算元棧[this] 
     0: aload_0
     // 使用dup指令複製棧頂元素併入棧,運算元棧[this,this]
     1: dup
     // 棧頂元素出棧並將它存入下標為1的區域性變數,現在棧上還剩下一個this物件引用。運算元棧[this]
     // 這裡透過一個臨時變數來儲存this的引用
     2: astore_1
     // 棧頂元素this出棧,monitorenter指令嘗試獲取this物件的監視器鎖,如果成功則繼續往下執行,
     // 如果已經有其他執行緒的執行緒持有,則進入等待狀態。
     // 運算元棧[this]
     3: monitorenter
     // 將this物件引用入棧,運算元棧[this] 
     4: aload_0
     // 使用dup指令複製棧頂元素併入棧,運算元棧[this,this]
     5: dup
     // 出棧,並獲取this引用的count欄位的值併入棧,
     // 假設count的值為x運算元棧[x,this]
     6: getfield      #2                  // Field count:I
     // 整數1入棧,運算元棧[1,x,this]
     9: iconst_1
     // 棧頂兩個元素出棧並相加,相加結果入棧,運算元棧[1+x,this]
    10: iadd
    // 1+x出棧、this出棧,將(x+1)的值賦值給this引用的count欄位
    11: putfield      #2                  // Field count:I
    // 將下標為1的區域性變數入棧,下標為1的區域性變數為this的引用,運算元棧[this]
    14: aload_1
    // 出棧,呼叫monitorexit釋放鎖
    15: monitorexit
    // 由Exception table發現,如果這裡接收到異常,則跳轉到19行,
    // 如果沒有異常,則跳轉到24行
    16: goto          24
    // 將異常結果儲存到下標為2的區域性變數中
    19: astore_2
    // 將下標為1的區域性變數入棧,下標為1的區域性變數為this的引用,運算元棧[this]
    20: aload_1
    // 出棧,呼叫monitorexit釋放鎖,運算元棧[this]
    21: monitorexit
    // 將下標為2的區域性變數入棧
    22: aload_2
    // 出棧並丟擲異常
    23: athrow
    // 
    24: return
  Exception table:
     from    to  target type
         4    16    19   any
        19    22    19   any
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0      25     0  this   Lcom/example/demo/test/T;
  StackMapTable: number_of_entries = 2
    frame_type = 255 /* full_frame */
      offset_delta = 19
      locals = [ class com/example/demo/test/T, class java/lang/Object ]
      stack = [ class java/lang/Throwable ]
    frame_type = 250 /* chop */
      offset_delta = 4

Java虛擬機器保證一個monitor一次最多隻能被一個執行緒佔有。monitorenter和monitorexit是兩個與監視器相關的位元組碼指令。當執行緒執行到monitorenter指令時,會嘗試獲取棧頂物件對應監視器(monitor)的所有權,也就是嘗試獲取物件的鎖。如果此時monitor沒有其他執行緒佔有,當前執行緒會成功獲取鎖,monitor計數器置為1。如果當前執行緒已經擁有了monitor的所有權,monitorenter指令也會順利執行,monitor計數器加1。如果其他執行緒擁有了monitor的所有權,當前執行緒會阻塞,直到monitor計數器變為0。

當執行緒執行monitorexit時,會將監視器計數器減1,計時器值等於0時,鎖被釋放,其他等待這個鎖的執行緒可以嘗試去獲取monitor的所有權。

編譯器必須保證無論同步程式碼塊中的程式碼以何種方式結束(正常退出或異常退出),程式碼中每次呼叫monitorenter必須執行對應的monitorexit指令。如果執行了monitorenter指令但沒有執行monitorexit指令,monitor一直被佔有,則其他執行緒沒有辦法獲取鎖。如果執行monitorexit的執行緒原本並沒有這個monitor的所有權,那monitorexit指令在執行時將丟擲IllegalMonitorStateException異常。

為了保證這一點,編譯器會自動生成一個異常處理器,這個異常處理器確保了方法正常退出和異常退出都能正常釋放鎖。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章