字节码基础

字节码基础

虚拟机栈和栈帧

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 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!