字节码指令集与解析举例(二)

操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令。

这类指令包括如下内容:

  1. 将一个或两个元素从栈顶弹出,并且直接废弃:poppop2

  2. 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dupdup2dup_x1dup2_x1dup_x2dup2_x2

  3. 将栈最顶端的两个Slot数值位置交换:swap。 Java虚拟机没有提供交换两个64位数据类型(long double)数值的指令。

  4. 指令nop,是一个非常特殊的指令,它的字节码为0x00。和汇编语言中的nop一样,它表示什么都不做。这条指令一般可用于调试、占位等。

这些指令属于通用型,对栈的压入或者弹出无需指明数据类型。

这些指令属于通用型,对栈的压入或者弹出无需指明数据类型。

说明:

  • 不带_x的指令是复制栈顶数据并压入栈顶。包括两个指令,dupdup2dup的系数代表要复制的Slot个数。
    • dup开头的指令用于复制1个Slot的数据。例如1个int或1个reference类型数据
    • dup2开头的指令用于复制2个Slot的数据。例如1个long,或2个int,或1个int+1个
  • _x的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令,dup_x1dup2_x1dup_x2dup2_x2。对于带_x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置。因此
    • dup_x1插入位置:1+1=2,即栈顶2个Slot下面
    • dup_x2插入位置:1+2=3,即栈顶3个Slot下面
    • dup2_x1插入位置:2+1=3,即栈顶3个Slot下面
    • dup2_x2插入位置:2+2=4,即栈顶4个Slot下面
  • pop:将栈顶的1个Slot数值出栈。例如1个short类型数值
  • pop2:将栈顶的2个Slot数值出栈。例如1个double类型数值,或者2个int类型数值
/**
 * 指令6:操作数栈管理指令
 */
public class StackOperateTest {
    public void print() {
        Object obj = new Object();
        String info = obj.toString();
    }
}
 0 new #2 <java/lang/Object>
 3 dup
 4 invokespecial #1 <java/lang/Object.<init>>
 7 astore_1
 8 aload_1
 9 invokevirtual #3 <java/lang/Object.toString>
12 astore_2
13 return

/**
 * 指令6:操作数栈管理指令
 */
public class StackOperateTest {
    public void print() {
        Object obj = new Object();
        // String info = obj.toString();
        obj.toString();
    }
}

变成pop指令了

 0 new #2 <java/lang/Object>
 3 dup
 4 invokespecial #1 <java/lang/Object.<init>>
 7 astore_1
 8 aload_1
 9 invokevirtual #3 <java/lang/Object.toString>
12 pop
13 return

/**
 * 指令6:操作数栈管理指令
 */
public class StackOperateTest {
    public long nextIndex() {
        return index++;
    }

    private long index = 0;
}

控制转移指令

程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为

  1. 比较指令
  2. 条件跳转指令
  3. 比较条件跳转指令
  4. 多条件分支跳转指令
  5. 无条件跳转指令

比较指令(这个是算术指令)

比较指令的说明:

  • 比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈。
  • 比较指令有: dcmpgdcmplfcmpgfcmpllcmp

与前面讲解的指令类似,首字符d表示double类型,f表示float,l表示long

  • 对于 double和float类型的数字,由于NaN的存在,各有两个版本的比较指令。以 float为例,有fcmpg和fcmpl两个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同。
  • 指令dcmpldcmpg也是类似的,根据其命名可以推测其含义,在此不再赘述。
  • 指令lcmp针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。

举例:

指令 fcmpgfcmpl都从栈中弹出两个操作数,并将它们做比较,设桟顶的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入;若v1>v2则压入1;若v1 < v2则压入-1。

两个指令的不同之处在于,如果遇到NaN值,fcmpg会压入1,而fcmpl会压入-1.

条件跳转指令

条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令选行栈顶元素的准备,然后进行条件跳转。

条件跳转指令有:ifeqifltifleifneifgtifgeifnullifnonnull。这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)。

它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置。

具体说明

指令 说明
ifeq equals 当栈顶int类型数值等于0时跳转
ifne not equals 当栈顶in类型数值不等于0时跳转
iflt lower than 当栈顶in类型数值小于0时跳转
ifle lower or equals 当栈顶in类型数值小于等于0时跳转
ifgt greater than 当栈顶int类型数组大于0时跳转
ifge greater or equals 当栈顶in类型数值大于等于0时跳转
ifnull 为null时跳转
ifnonnull 不为null时跳转

注意:

  1. 与前面运算规则一致

    • 对于boolean、 byte、 char、short类型的条件分支比较操作,都是使用int类型的比较指令完成
    • 对于long、 float、double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转
  2. 由于各类型的比较最终都会转为int类型的比较操作,所以Java虚拟机提供的int类型的条件分支指令是最为丰富和强大的。

ifeq举例

/**
 * 指令7:控制转移指令
 */
public class IfSwitchGotoTest {
    public void compare1() {
        int a = 0;
        if (a == 0) {
            a = 10;
        } else {
            a = 20;
        }
    }
}
 0 iconst_0
 1 istore_1
 2 iload_1
 3 ifne 12 (+9)
 6 bipush 10
 8 istore_1
 9 goto 15 (+6)
12 bipush 20
14 istore_1
15 return

ifnonnull举例

/**
 * 指令7:控制转移指令
 */
public class IfSwitchGotoTest {
    public boolean compareNull(String str) {
        if (str == null) {
            return true;
        } else {
            return false;
        }
    }
}
0 aload_1
1 ifnonnull 6 (+5)
4 iconst_1
5 ireturn
6 iconst_0
7 ireturn

结合比较指令

/**
 * 指令7:控制转移指令
 */
public class IfSwitchGotoTest {
    /**
     * 结合比较指令
     */
    public void compare2() {
        float f1 = 9;
        float f2 = 10;
        System.out.println(f1 < f2);
    }

    public void compare3() {
        int i1 = 10;
        long l1 = 20;
        System.out.println(i1 < l1);
    }

    public int compare4(double d) {
        if (d > 50.0) {
            return 1;
        } else {
            return -1;
        }
    }
}

println有参数为boolean的方法,所以这里打印的不是数字而是布尔值


compare3:出现了宽化类型转换

 0 bipush 10
 2 istore_1
 3 ldc2_w #6 <20>
 6 lstore_2
 7 getstatic #4 <java/lang/System.out>
10 iload_1
11 i2l
12 lload_2
13 lcmp
14 ifge 21 (+7)
17 iconst_1
18 goto 22 (+4)
21 iconst_0
22 invokevirtual #5 <java/io/PrintStream.println>
25 return

compare4:

 0 dload_1
 1 ldc2_w #8 <50.0>
 4 dcmpl
 5 ifle 10 (+5)
 8 iconst_1
 9 ireturn
10 iconst_m1
11 ireturn

比较条件跳转指令

比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。

这类指令有:if_icmpeqif_icmpneif_icmpltif_icmpgtif_icmpleificmpgeif_ acmpeqif_acmpne其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数操作(也包括 short和byte类型),以字符“a”开头的指令表示对象引用的比较。

具体说明

指令 说明
if_icmpeq 比较栈顶两int类型数值大小,当前者等于后者时跳转
if_icmpne 比较栈顶两int类型数值大小,当前者不等于后者时跳转
if_icmplt 比较栈顶两int类型数值大小,当前者小于后者时跳转
if_icmple 比较栈顶两int类型数值大小,当前者小于等于后者时跳转
if_icmpgt 比较栈顶两int类型数值大小,当前者大于后者时跳转
if_icmpge 比较栈顶两int类型数值大小,当前者大于等于后者时跳转
if_acmpeq 比较栈顶两引用类型数值,当结果相等时跳转
if_acmpne 比较栈顶两引用类型数值,当结果不相等时跳转

这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一条语句。

/**
 * 指令7:控制转移指令
 */
public class IfSwitchGotoTest {
    /**
     * 比较条件跳转指令
     */
    public void ifCompare1() {
        int i = 10;
        int j = 20;
        System.out.println(i < j);
    }

    public void ifCompare2() {
        short s1 = 9;
        byte b1 = 10;
        System.out.println(s1 > b1);
    }

    public void ifCompare3() {
        Object obj1 = new Object();
        Object obj2 = new Object();
        System.out.println(obj1 == obj2);
        System.out.println(obj1 != obj2);
    }
}

ifComapre1

 0 bipush 10
 2 istore_1
 3 bipush 20
 5 istore_2
 6 getstatic #4 <java/lang/System.out>
 9 iload_1
10 iload_2
11 if_icmpge 18 (+7)
14 iconst_1
15 goto 19 (+4)
18 iconst_0
19 invokevirtual #5 <java/io/PrintStream.println>
22 return

ifCompare2:short和byte也使用

 0 bipush 9
 2 istore_1
 3 bipush 10
 5 istore_2
 6 getstatic #4 <java/lang/System.out>
 9 iload_1
10 iload_2
11 if_icmple 18 (+7)
14 iconst_1
15 goto 19 (+4)
18 iconst_0
19 invokevirtual #5 <java/io/PrintStream.println>
22 return

ifCompare3

 0 new #10 <java/lang/Object>
 3 dup
 4 invokespecial #1 <java/lang/Object.<init>>
 7 astore_1
 8 new #10 <java/lang/Object>
11 dup
12 invokespecial #1 <java/lang/Object.<init>>
15 astore_2
16 getstatic #4 <java/lang/System.out>
19 aload_1
20 aload_2
21 if_acmpne 28 (+7)
24 iconst_1
25 goto 29 (+4)
28 iconst_0
29 invokevirtual #5 <java/io/PrintStream.println>
32 getstatic #4 <java/lang/System.out>
35 aload_1
36 aload_2
37 if_acmpeq 44 (+7)
40 iconst_1
41 goto 45 (+4)
44 iconst_0
45 invokevirtual #5 <java/io/PrintStream.println>
48 return

多条件分支跳转

多条件分支跳转指令是专为switch-case语句设计的,主要有tableswitchlookupswitch

指令名称 描述
tableswitch 用于switch条件跳转,case值连续
lookupswitch 用于switch条件跳转,case值不连续

从助记符上看,两者都是switch语句的实现,它们的区别:

  • tableswitch要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高
  • 指令lookupswitch内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低

指令tableswitch的示意图如下图所示。由于tableswitch的case值是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。

指令lookupswitch处理的是离散的case值,但是出于效率考虑,将case-offset对按照case值大小排序,给定index时,需要査找与index相等的case,获得其offset,如果找不到则跳转到default。指令lookupswitch如下图所示。

示例

/**
 * 指令7:控制转移指令
 */
public class IfSwitchGotoTest {
    /**
     * 3. 多条件分支跳转
     *
     * @param select
     */
    public void switch1(int select) {
        int num;
        switch (select) {
            case 1:
                num = 10;
                break;
            case 2:
                num = 20;
                break;
            case 3:
                num = 30;
                break;
            default:
                num = 40;
        }
    }

    public void switch2(int select) {
        int num;
        switch (select) {
            case 100:
                num = 10;
                break;
            case 500:
                num = 20;
                break;
            case 200:
                num = 30;
                break;
            default:
                num = 40;
        }
    }

    /**
     * JDK7新特性:引入String类型
     *
     * @param season
     */
    public void switch3(String season) {
        switch (season) {
            case "SPRING":
                break;
            case "SUMMER":
                break;
            case "AUTUMN":
                break;
            case "WINTER":
                break;
        }
    }
}

switch1是连续的,所以从字节码中可以看出是使用的tableswitch指令

 0 iload_1
 1 tableswitch 1 to 3	1:  28 (+27)
	2:  34 (+33)
	3:  40 (+39)
	default:  46 (+45)
28 bipush 10
30 istore_2
31 goto 49 (+18)
34 bipush 20
36 istore_2
37 goto 49 (+12)
40 bipush 30
42 istore_2
43 goto 49 (+6)
46 bipush 40
48 istore_2
49 return

而switch2不是连续的,所以使用的是lookupswitch

 0 iload_1
 1 lookupswitch 3
	100:  36 (+35)
	200:  48 (+47)
	500:  42 (+41)
	default:  54 (+53)
36 bipush 10
38 istore_2
39 goto 57 (+18)
42 bipush 20
44 istore_2
45 goto 57 (+12)
48 bipush 30
50 istore_2
51 goto 57 (+6)
54 bipush 40
56 istore_2
57 return

注意String类型的字节码:先通过哈希值,然后通过equals判断

  0 aload_1
  1 astore_2
  2 iconst_m1
  3 istore_3
  4 aload_2
  5 invokevirtual #11 <java/lang/String.hashCode>
  8 lookupswitch 4
	-1842350579:  52 (+44)
	-1837878353:  66 (+58)
	-1734407483:  94 (+86)
	1941980694:  80 (+72)
	default:  105 (+97)
 52 aload_2
 53 ldc #12 <SPRING>
 55 invokevirtual #13 <java/lang/String.equals>
 58 ifeq 105 (+47)
 61 iconst_0
 62 istore_3
 63 goto 105 (+42)
 66 aload_2
 67 ldc #14 <SUMMER>
 69 invokevirtual #13 <java/lang/String.equals>
 72 ifeq 105 (+33)
 75 iconst_1
 76 istore_3
 77 goto 105 (+28)
 80 aload_2
 81 ldc #15 <AUTUMN>
 83 invokevirtual #13 <java/lang/String.equals>
 86 ifeq 105 (+19)
 89 iconst_2
 90 istore_3
 91 goto 105 (+14)
 94 aload_2
 95 ldc #16 <WINTER>
 97 invokevirtual #13 <java/lang/String.equals>
100 ifeq 105 (+5)
103 iconst_3
104 istore_3
105 iload_3
106 tableswitch 0 to 3	0:  136 (+30)
	1:  139 (+33)
	2:  142 (+36)
	3:  145 (+39)
	default:  145 (+39)
136 goto 145 (+9)
139 goto 145 (+6)
142 goto 145 (+3)
145 return

无条件跳转

目前主要的无条件跳转指令为goto。指令goto接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。

如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令goto_w,它和goto有相同的作用,但是它接收4个宇节的操作数,可以表示更大的地址范围。

指令jsrjsr_wret虽然也是无条件跳转的,但主要用于try-finally语句,且已经被虚拟机逐渐废弃,故不在这里介绍这两个指令。

指令名称 描述
goto 无条件跳转
goto_w 无条件跳转(宽索引)
jsr 跳转至指定16位offset位置,并将jsr下条指令地址压入栈顶
jsr_w 跳转至指定32位offer位置,并将jsr_w下条指令地址压入栈顶
ret 返回至由指定的局部变量所给出的指令位置(一般与jsr、jsr_w联合使用)
/**
 * 指令7:控制转移指令
 */
public class IfSwitchGotoTest {
    /**
     * 4. 无条件跳转指令
     */
    public void whileInt() {
        int i = 0;
        while (i < 100) {
            String s = "atguigu";
            i++;
        }
    }

    public void whileDouble() {
        double d = 0.0;
        while (d < 100.1) {
            String s = "atguigu";
            d++;
        }
    }

    public void printFor() {
        short i;
        for (i = 0; i < 100; i++) {
            String s = "atguigu";
        }
    }
}

whileInt

 0 iconst_0
 1 istore_1
 2 iload_1
 3 bipush 100
 5 if_icmpge 17 (+12)
 8 ldc #17 <atguigu>
10 astore_2
11 iinc 1 by 1
14 goto 2 (-12)
17 return

思考:

  • whileTest和forTest的字节码是一样的,区别是i的作用域不同
  • doWhileTest中i++至少会执行一次
/**
 * 思考:如下两个方法的操作有何不同?
 */
public void whileTest() {
    int i = 1;
    while (i <= 100) {
        i++;
    }
    // 可以继续使用i
}

public void forTest() {
    for (int i = 1; i <= 100; i++) {
    }
    // 不可以继续使用i
}

/**
 * 更进一步
 */
public void doWhileTest() {
    int i = 1;
    do {
        i++;
    } while (i <= 100);
}

异常处理指令

抛出异常指令

athrow指令

在Java程序中显式抛出异常的操作 (throw语句)都是由athrow指令来实现。

除了使用throw语句显式抛出异常情况之外,JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在idivldiv指令中抛出 ArithmeticException异常。

注意

正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时,Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。

异常及异常的处理:

  • 过程一:异常对象的生成过程→throw(手动/自动)→指令:athrow
  • 过程二:异常的处理:抓抛模型。try-catch-finally→使用异常表
import java.io.IOException;

/**
 * 指令8:异常处理
 */
public class ExceptionTest {
    public void throwZero(int i) {
        if (i == 0) {
            throw new RuntimeException("参数值为0");
        }
    }

    public void throwOne(int i) throws RuntimeException, IOException {
        if (i == 0) {
            throw new RuntimeException("参数值为1");
        }
    }
}

throwZero方法执行过程

throwOne方法执行字节码指令,和上面几乎相同

 0 iload_1
 1 ifne 14 (+13)
 4 new #2 <java/lang/RuntimeException>
 7 dup
 8 ldc #5 <参数值为1>
10 invokespecial #4 <java/lang/RuntimeException.<init>>
13 athrow
14 return

但是多了一个异常表


public void throwsArithmetic() {
    int i = 10;
    int j = i / 0;
    System.out.println(j);
}

可以看到,字节码指令中没有athrow,JVM会自动抛

 0 bipush 10
 2 istore_1
 3 iload_1
 4 iconst_0
 5 idiv
 6 istore_2
 7 getstatic #6 <java/lang/System.out>
10 iload_2
11 invokevirtual #7 <java/io/PrintStream.println>
14 return

异常处理与异常表

处理异常

在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(早期使用jsrret指令),而是采用异常表来完成的。

异常表

如果一个方法定义了一个try-catch或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者finally块的信息。异常表保存了每个异常处理信息。比如:

  • 起始位置
  • 结束位置
  • 程序计数器记录的代码处理的偏移地址
  • 被捕获的异常类在常量池中的索引

当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。

不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标。

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

/**
 * 指令8:异常处理
 */
public class ExceptionTest {
    public void tryCatch() {
        try {
            File file = new File("/hello.txt");
            FileInputStream fileInputStream = new FileInputStream(file);
            String info = "hello";
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
    }
}

如果出现异常,根据异常表执行字节码指令


try-finally代码执行过程举例

/**
 * 指令8:异常处理
 */
public class ExceptionTest {
    /**
     * 思考:如下方法返回结果为多少?
     *
     * @return
     */
    public static String func() {
        String str = "hello";
        try {
            return str;
        } finally {
            str = "atguigu";
        }
    }

    public static void main(String[] args) {
        System.out.println(func());
    }
}
hello

Process finished with exit code 0

同步控制指令

Java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的。

方法级的同步

方法级的同步:是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法

当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置。

  • 如果设置了,执行线程将先持有同步锁,然后执行方法。最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁
  • 在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得同一个锁。
  • 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。
/**
 * 指令9:同步控制指令
 */
public class SynchronizedTest {
    private int i = 0;

    public synchronized void add() {
        i++;
    }

    private Object obj = new Object();

    public void subtract() {
        synchronized (obj) {
            i--;
        }
    }
}

对于add方法,可以从方法访问标识中看到这是一个同步方法,但是在字节码指令方面和不加synchronized相同,是一个隐式的同步机制。

 0 aload_0
 1 dup
 2 getfield #2 <SynchronizedTest.i>
 5 iconst_1
 6 iadd
 7 putfield #2 <SynchronizedTest.i>
10 return

这段代码和普通的无同步操作的代码没有什么不同,没有使用monitorentermonitorexit进行同步区控制。这是因为,对于同步方法而言,当虚拟机通过方法的访问标示符判断是一个同步方法时,会自动在方法调用前进行加锁,当同步方法执行完毕后,不管方法是正常结束还是有异常抛出,均会由虚拟机释放这个锁。因此,对于同步方法而言,monitorentermonitorexit指令是隐式存在的,并未直接出现在字节码中。

方法内指定指令序列的同步

同步一段指令集序列:通常是由Java中的synchronized语句块来表示的。JVM的指令集有monitorentermonitorexit两条指令来支持synchronized关键字的语义。

当一个线程进入同步代码块时,它使用monitorenter指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块。

当线程退出同步块时,需要使用monitorexit声明退出。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。

指令monitorentermonitorexit在执行时,都需要在操作数栈顶压入对象,之后monitorentermonitorexit的锁定和释放都是针对这个对象的监视器进行的。

下图展示了监视器如何保护临界区代码不同时被多个线程访问,只有当线程4离开临界区后,线程1、2、3才有可能进入。

substrct解析

JVM官方对于两个指令的解释

posted @ 2021-03-10 15:40  我係死肥宅  阅读(433)  评论(0)    收藏  举报