JVM3️⃣字节码技术②不同代码的字节码指令

1、整数自增

Java 整数可以自增(++ii++)和自减( --ii--

  1. iinc:int 类型的自增指令(int increament)
    • 注意直接在局部变量表的 slot 上运算,无需加载到操作数栈
    • 格式iinc slot 增量
  2. ++i 或 ++i:区别在于 iload 和 iinc 的执行顺序。
  3. 自增或自减:区别在于增量的正负号。

1.1、示例

Java 文件

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

字节码:如下,仅展示方法部分

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
    stack=2, locals=3, args_size=1
        0: bipush 10
        2: istore_1
        3: iload_1
        4: iinc 1, 1
        7: iinc 1, 1
        10: iload_1
        11: iadd
        12: iload_1
        13: iinc 1, -1
        16: iadd
        17: istore_2
        18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
        21: iload_1
        22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
        25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
        28: iload_2
        29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
        32: return
    LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 18
        line 11: 25
        line 12: 32
    LocalVariableTable:
        Start	Length    Slot	Name	Signature
            0		33		0	args	[Ljava/lang/String;
            3		30		1	a		I
           18 		15		2	b		I

1.2、分析

主线程开始运行,分配栈帧内存

  • 操作数栈:stack = 2,容量为 2。

  • 局部变量表:locals = 3,长度为 3。

  1. bipush 10:将整数 10 入栈

  2. istore_1:将栈顶数据(int,10),保存到局部变量表下标为 1 的 slot

    image-20220301120726219
  3. iload_1:将局部变量表中 slot 1 的数据(int,10),加载到操作数栈的栈顶

  4. iinc 1, 1:将 slot 1 的整数自增 1(10 → 11)

    image-20220301121008869
  5. iinc 1, 1:将 slot 1 的整数自增 1(11 → 12)

  6. iload_1:将局部变量表中 slot 1 的数据(int,12),加载到操作数栈的栈顶

    image-20220301121206817
  7. iadd:将操作数栈的两个整数相加并出栈,求和结果(22)入栈

  8. iload_1:将局部变量表中 slot 1 的数据(int,12),加载到操作数栈的栈顶

  9. iinc 1, -1:将 slot 1 的整数自增 -1(12 → 11)

    image-20220301121828572
  10. iadd:将操作数栈的两个整数相加并出栈,求和结果(34)入栈

  11. istore_2:将栈顶数据(int,34),保存到局部变量表下标为 2 的 slot

    image-20220301122233485

到此,a 和 b 的赋值情况已分析结束。(省略打印语句的分析,可以往前一节看)

最终答案a == 11,b == 34

2、条件、循环

2.1、条件判断

public static void main(String[] args) {
    int a = 0;
    if(a == 0) {
        a = 10;
    } else {
        a = 20;	
    }
}

字节码文件:仅展示方法部分

  • ifne:判断栈顶数据是否 != 0

  • goto跳转到指定行指令

    0: iconst_0		// 将整数 0 入栈
    1: istore_1		// 将栈顶数据(0)保存到局部变量表 slot 1
    2: iload_1		// 加载局部变量表 slot 1 数据(0)
    3: ifne 12		// 判断 “!= 0”。成立则跳转到第 12 行指令,不成立则执行下一行指令
    6: bipush 10	// 将整数 10 入栈
    8: istore_1		// 将栈顶数据(10)保存到局部变量表 slot 1
    9: goto 15		// 跳转到第 15 行指令
    12: bipush 20	// 将整数 20 入栈
    14: istore_1	// 将栈顶数据(20)保存到局部变量表 slot 1
    15: return
    

2.2、循环控制

在循环控制中,使用的也是 if 语句的字节码指令。

do-while

public static void main(String[] args) {
    int a = 0;
    do {
        a++;
    } while (a < 10);
}

if_icmplt:判断栈顶数据两个元素,是否 < 0(top.next - top)

0: iconst_0		// 将整数 0 入栈
1: istore_1		// 将栈顶数据(0)保存到局部变量表 slot 1
2: iinc 1, 1	// 将 slot 1 的整数自增 1
5: iload_1		// 加载局部变量表 slot 1 数据(1)
6: bipush 10	// 将整数 10 入栈
8: if_icmplt 2	// 比较栈顶两个数据,判断“< 0”。成立则跳转到第 2 行指令
11:return

while 和 for

以下两种方式的代码,编译后的字节码完全相同。

// while
public static void main(String[] args) {
    int a = 0;
    while (a < 10) {
        a++;
    }
}

// for
public static void main(String[] args){
    for (int i = 0; i < 10; i++){
    }
}

if_icmpge:判断栈顶数据两个元素,是否 >= 0(top.next - top)

0: iconst_0		// 将整数 0 入栈
1: istore_1		// 将栈顶数据(0)保存到局部变量表 slot 1
2: iload_1		// 加载局部变量表 slot 1 数据(0)
3: bipush 10	// 将整数 10 入栈
5: if_icmpge 14 // 比较栈顶两个数据,判断“>= 0”。成立则跳转到第 14 行指令
8: iinc 1, 1	// 将 slot 1 的整数自增 1
11: goto 2		// 跳转到第 2 行指令
14: return

2.3、思考题

  • :循环结束时 x 的值。

    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
        }
    }
    
  • 分析:对 x = x++ 进行分析(x 初值为 0,位于 slot2)

    iload_2			// 加载局部变量表 slot 2 数据(x,此时为 0)
    iinc 2,1		// 将 slot 2 整数自增 1(x:0 → 1)
    istore_2		// 将栈顶数据(0)存储到 slot 2(将 x 覆盖为0)
    

结论:每次循环中,x 先自增 1,再被覆盖为 0。

最终结果:0

3、构造方法

<cinit>()V

类构造方法:针对静态结构类加载的初始化阶段被调用

public class Demo {
    static int i = 10;
    
    static { i = 20; }
    
    static { i = 30; }
}

编译器按从上往下的顺序,收集所有静态结构(静态代码块,静态成员)的赋值语句,合并为<cinit>()V 方法

0: bipush 10
2: putstatic #2		// Field i:I
5: bipush 20
7: putstatic #2		// Field i:I
10: bipush 30
12: putstatic #2	// Field i:I
15: return

<init>()V

构造方法:针对非静态结构,类加载的初始化阶段被调用。

public class Demo {
    private String a = "s1";
    
    { b = 20; }
    
    private int b = 10;
    
    { a = "s2"; }
    
    public Demo3_8_2(String a, int b) {
        this.a = a;
        this.b = b;
    }
    
    public static void main(String[] args) {
        Demo3_8_2 d = new Demo3_8_2("s3", 30);
        System.out.println(d.a);
        System.out.println(d.b);
    }
}
  • 编译器按从上往下的顺序,收集所有非静态结构(代码块,成员变量)的赋值语句,合并为<cinit>()V方法

  • 原始构造方法(与类同名的构造函数),也会被收集进去,但构造函数位于最后

    // 父类构造方法
    0: aload_0
    1: invokespecial #1	// super.<init>()V
    // 非静态结构
    4: aload_0
    5: ldc #2			// "s1"
    7: putfield #3		// this.a
    10: aload_0
    11: bipush 20
    13: putfield #4		// this.b
    16: aload_0
    17: bipush 10
    19: putfield #4
    22: aload_0
    23: ldc #5			// "s2"
    25: putfield #3
    // 构造函数
    28: aload_0
    29: aload_1
    30: putfield #3
    33: aload_0
    34: iload_2
    35: putfield #4
    38: return
    

4、方法调用

public class Demo {
	public Demo() { }
    
    private void test1() { }
    
    private final void test2() { }
    
    public void test3() { }
    
    public static void test4() { }
    
    public static void main(String[] args) {
        Demo d = new Demo();
        
        d.test1();
        d.test2();
        d.test3();
        d.test4();
        
        Demo.test4();
    }
}
  • new:创建对象。分配堆内存,将对象引用压入操作数栈

  • dup复制操作数栈的栈顶数据,此时栈中有 2 个相同的对象引用(一个用于初始化,一个用于存储

  • 有关方法调用的说明,看本文 2.2 的字节码指令。

    0: new #2
    3: dup
    4: invokespecial #3		// Method "<init>":()V
    7: astore_1
    8: aload_1
    9: invokespecial #4		// Method test1:()V
    12: aload_1
    13: invokespecial #5	// Method test2:()V
    16: aload_1
    17: invokevirtual #6	// Method test3:()V
    20: aload_1
    21: pop
    22: invokestatic #7		// Method test4:()V
    25: invokestatic #7		// Method test4:()V
    28: return
    

5、异常

5.1、try-catch

public static void main(String[] args) {
    int i = 0;
    try {
        i = 10;
    } catch (Exception e) {
        i = 20;
    }
}

字节码

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
        stack=1, locals=3, args_size=1
            0: iconst_0
            1: istore_1
            // try
            2: bipush 10
            4: istore_1
            5: goto 12
            // catch
            8: astore_2
            9: bipush 20
            11: istore_1
            12: return
        // 异常表
		Exception table:
    		from	to	target	type
    			2	5		8	Class java/lang/Exception
		LineNumberTable: ...
    	LocalVariableTable:
            Start  Length  Slot  Name	Signature
           	9		3		2		e 	Ljava/lang/Exception;
           	0		13		0	 args 	[Ljava/lang/String;
           	2		11		1		i 	I
    	StackMapTable: ...
    MethodParameters: ...
}
  • 异常表(Exception table),异常中特有的结构
    • from/to:可能出现异常的代码,即 try 块。【左闭右开 [from, to)】
    • target:发生匹配到的异常时的跳转行号。
    • type:异常类型
  • 异常监测监测 [from, to) 的代码(try 块)
    • 发生与 catch 块匹配的异常时,跳转到 target(第 8 行)
      • a_store2:将异常对象的引用放入 slot2。
      • 执行该 catch 块的内容,结束。
    • 没有异常,执行完 try 块代码后,跳出 try-catch 块

5.2、多个 catch

public static void main(String[] args) {
    int i = 0;
    try {
        i = 10;
    } catch (ArithmeticException e) {
        i = 30;
    } catch (NullPointerException e) {
        i = 40;
    } catch (Exception e) {
        i = 50;
    }
}

字节码

stack=1, locals=3, args_size=1
    0: iconst_0
    1: istore_1
	// try
    2: bipush 10
    4: istore_1
    5: goto 26
    // catch
    8: astore_2
    9: bipush 30
    11: istore_1
    12: goto 26
    // catch
    15: astore_2
    16: bipush 40
    18: istore_1
    19: goto 26
    // catch
    22: astore_2
    23: bipush 50
    25: istore_1
    26: return
Exception table:
    from	to	target	type
	2		5	8 		Class java/lang/ArithmeticException
	2		5	15		Class java/lang/NullPointerException
	2		5	22		Class java/lang/Exception
LineNumberTable: ...
LocalVariableTable:
    Start	Length	Slot	Name	Signature
    9		3 		2		e		Ljava/lang/ArithmeticException;
    16		3 		2		e		Ljava/lang/NullPointerException;
    23		3 		2		e		Ljava/lang/Exception;
    0		27		0		args	[Ljava/lang/String;
    2		25		1		i		I
StackMapTable: ...
MethodParameters: ...

异常表(Exception table)

  • 相比一个 single-catch 块,增加了相应个数的异常记录
  • 每个 catch 块都有一组字节码指令。

异常监测检测 [from, to) 的代码(try 块)

  1. 发生与 catch 块匹配的异常时,跳转到 target(按异常类型的定义顺序匹配)
    • ArithmeticException:跳转到第 8 行
    • NullPointerException:跳转到第 15 行
    • Exception:跳转到第 22 行
  2. astore_2:将异常对象的引用放入 slot2。
    • 由于只会匹配并进入一个异常分支,所以 slot2 公用。
  3. 执行该 catch 块的内容,结束

若没有异常,执行完 try 块代码后,跳出 try-catch 块

5.3、multi-catch

public static void main(String[] args) {
    try {
        Method test = Demo3_11_3.class.getMethod("test");
        test.invoke(null);
    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
}

public static void test() {
    System.out.println("ok");
}

字节码

stack=3, locals=2, args_size=1
    0: ldc #2
    2: ldc #3
    4: iconst_0
    5: anewarray #4
    8: invokevirtual #5
    11: astore_1
    12: aload_1
    13: aconst_null
    14: iconst_0
    15: anewarray #6
    18: invokevirtual #7
    21: pop
    22: goto 30
    // catch
    25: astore_1
    26: aload_1
    27: invokevirtual #11 // e.printStackTrace:()V
    30: return
Exception table:
    from	to	target	type
    0		22		25	Class java/lang/NoSuchMethodException
    0		22		25	Class java/lang/IllegalAccessException
    0		22		25	Class java/lang/reflect/InvocationTargetException
LineNumberTable: ...
LocalVariableTable:
    Start	Length	Slot	Name	Signature
    12		10		1		test	Ljava/lang/reflect/Method;
    26		4 		1		e		Ljava/lang/ReflectiveOperationException;
    0		31		0		args	[Ljava/lang/String;
StackMapTable: ...
MethodParameters: ...

异常表(Exception table)

  • 相比一个 single-catch 块,增加了相应个数的异常记录。
  • 相比多个 single-catch 块,只有一组 catch 块的字节码指令

异常监测检测 [from, to) 的代码(try 块)

  1. 发生与 multi-catch 块匹配的异常时,跳转到 target(对异常类型的定义顺序没有要求,但不能是父子类)
  2. astore_1:将异常对象的引用放入 slot1。
  3. 执行该 catch 块的内容,结束。

若没有异常,执行完 try 块代码后,跳出 try-catch 块。

5.4、finally

public static void main(String[] args) {
    int i = 0;
    try {
        i = 10;
    } catch (Exception e) {
        i = 20;
    } finally {
        i = 30;
    }
}

字节码

Code:
    stack=1, locals=4, args_size=1
        0: iconst_0
        1: istore_1
		// try
        2: bipush 10
        4: istore_1
           	// finally
        5: bipush 30
        7: istore_1
        8: goto 27
        // catch Exception
        11: astore_2
        12: bipush 20
        14: istore_1
            // finally
        15: bipush 30
        17: istore_1
        18: goto 27
        // catch any
        21: astore_3
            // finally
        22: bipush 30
        24: istore_1
        25: aload_3
        26: athrow
        27: return
    Exception table:
        from	to	target	type
        2		5		11	Class java/lang/Exception
        2		5		21	any		// 剩余的异常类型,比如 Error
        11		15		21	any		// 剩余的异常类型,比如 Error
    LineNumberTable: ...
    LocalVariableTable:
        Start	Length	Slot	Name	Signature
		 12		   3	 2		e		Ljava/lang/Exception;
		  0		  28	 0		args	[Ljava/lang/String;
		  2		  26	 1		i		I
    StackMapTable: ...
MethodParameters: ...
  1. finally 块的字节码指令,被复制并分别放入 try 块、catch 块
  2. 增加 any 异常,表示可能出现的其它异常类型(如 Error)
    • catch any 异常块中,也有 finally 代码的字节码指令;
    • 该异常会被存储在一个“无名” slot,并被 athrow 指令抛出。

5.5、finally 对返回值影响

finally 块中出现 return 的情况。

:方法的返回值是什么?10 还是 20 ?

public class Demo {
    public static void main(String[] args) {
        int result = test();
    }
    public static int test() {
        try {
            return 10;
        } finally {
            return 20;
        }
    }
}

在 Java SE 中,我们知道在方法返回之前会执行 finally 块,所以此时肯定返回的是 20。

从字节码的角度分析。

stack=1, locals=2, args_size=0
    // try
    0: bipush 10
    2: istore_0
        // finally
    3: bipush 20
    5: ireturn
	// catch any
    6: astore_1
        // finally
    7: bipush 20
    9: ireturn
Exception table:
    from	to	target	type
	0		3		6	any
LineNumberTable: ...
StackMapTable: ...
  1. finally 块代码对应的字节码指令,被复制并分别放入 try 块、catch any 块。
    • ireturn将栈顶的整数作为返回值返回
  2. 与上一个案例中的 finally 相比,any 异常不会被 athrow 抛出
  3. 结论当 finally 块中出现 return,异常会被吞掉

再来看 finally 块中没有 return,但修改作为返回值的变量的情况。

:方法的返回值是什么?10 还是 20 ?

public class Demo {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }
    
    public static int test() {
        int i = 10;
        try {
            return i;
        } finally {
            i = 20;
        }
    }
}

字节码

stack=1, locals=3, args_size=0
    0: bipush 10
    2: istore_0
    // try
    3: iload_0
    4: istore_1		// 将 i 放入 slot1 作为返回值
        // finally
    5: bipush 20
    7: istore_0		// 修改的是 slot0,不影响返回值
    // return
    8: iload_1		// 加载的是 slot1
    9: ireturn		// 返回栈顶数据(10)
    // catch any
    10: astore_2
        // finally
    11: bipush 20
    13: istore_0
    // 抛异常
    14: aload_2
    15: athrow
Exception table:
    from	to	target	type
    3		5		10	any
LineNumberTable: ...
LocalVariableTable:
	Start	Length	Slot	Name	Signature
		3		13	0		i		I
StackMapTable: ...
  1. finally 块代码对应的字节码指令,被复制并分别放入 try 块、catch any 块。
  2. 注意
    1. slot0 和 slot1 都存储了变量 i,分别用于值的修改和返回值。
    2. slot0:当 finally 块中修改 i 时,修改的是 slot0 的 i;
    3. slot1:当方法返回时,返回的是 slot1 的i。
    4. 说明:i 值不会被修改,相当于锁定了返回值
  3. 结论当 finally 块中没有 return,try 块中固定了返回值,不会被 finally 影响!

6、同步(Monitor

public static void main(String[] args) {
    Object lock = new Object();
    
    synchronized (lock) {
        System.out.println("ok");
    }
}

字节码

stack=2, locals=4, args_size=1
    // 实例化对象并初始化
    0: new #2
    3: dup
    4: invokespecial #1
    7: astore_1
    // 开始同步代码块
    8: aload_1
    9: dup
    10: astore_2			// 复制一份lock引用,用于解锁
        // 加锁
    11: monitorenter
    12: getstatic #3		// <- System.out
    15: ldc #4				// <- "ok"
    17: invokevirtual #5	// invokevirtual println:(Ljava/lang/String;)V
        // 解锁
    20: aload_2
    21: monitorexit
    22: goto 30
	// catch any
    25: astore_3
        // 解锁
    26: aload_2
    27: monitorexit
    // 抛异常
    28: aload_3
    29: athrow
    30: return
Exception table:
    from	to	target	type
    12		22		25	any
    25		28		25	any
LineNumberTable: ...
LocalVariableTable:
    Start	Length	Slot	Name	Signature
    	0		31	0		args	[Ljava/lang/String;
    	8		23	1		lock	Ljava/lang/Object;
StackMapTable: ...
  1. 进入同步代码块后,会复制一份被加锁的对象并存储在局部变量表,用于解锁
  2. monitorenter 加锁,monitorexit 解锁。加锁和解锁指令成对出现
  3. 不论是否会出现异常,monitor 都会将对象引用解锁。
  4. 方法级别的 synchronized 不会在字节码指令中有所体现
posted @ 2022-03-01 16:10  Jaywee  阅读(54)  评论(0编辑  收藏  举报

👇