JVM字节码之字节码指令三

  • 异常处理指令
  • 线程同步指令

异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Main {
public static void main(String[] args) {
try {
test1();
} catch (IndexOutOfBoundsException e) {
test2();
} catch (Exception e) {
test2();
} finally {
test3();
}
}

private static final void test1() {

}

private static final void test2() {

}

private static final void test3() {

}
}

生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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: invokestatic #2 // Method test1:()V
3: invokestatic #3 // Method test3:()V
6: goto 35

9: astore_1
10: invokestatic #5 // Method test2:()V
13: invokestatic #3 // Method test3:()V
16: goto 35

19: astore_1
20: invokestatic #5 // Method test2:()V
23: invokestatic #3 // Method test3:()V
26: goto 35

29: astore_2
30: invokestatic #3 // Method test3:()V
33: aload_2
34: athrow
35: return
Exception table:
from to target type
0 3 9 Class java/lang/IndexOutOfBoundsException
0 3 19 Class java/lang/Exception
0 3 29 any
9 13 29 any
19 23 29 any
LocalVariableTable:
Start Length Slot Name Signature
10 3 1 e Ljava/lang/IndexOutOfBoundsException;
20 3 1 e Ljava/lang/Exception;
0 36 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 4
frame_type = 73 /* same_locals_1_stack_item */
stack = [ class java/lang/IndexOutOfBoundsException ]
frame_type = 73 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 73 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 5 /* same */

可以看出多了这些东西 goto指令,athrow指令, Exception tableStackMapTable, 并且局部变量表里面增加了一条记录.

  • goto : 用于跳转
  • athrow : 通过 athrow 可以知道局部变量表中位置为 2 的变量是一个异常
  • Exception table : 中的from和to限定了异常的处理范围, target表示跳转的位置, type表示可以捕获的异常类型
  • StackMapTable : 为了提高JVM在类型检查的验证过程的效率, 在字节码规范中添加了Stack Map Table属性, number_of_entries代表了栈图frame的个数

finally中执行了test3()方法, 可以看到之所以finally可以保证执行, 是因为finally框住的逻辑被拷贝了几份, 分别放在trycatch代码块下面, 伪代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
try {
test1();
test3();
} catch (IndexOutOfBoundsException e) {
test2();
test3();
} catch (Exception e) {
test2();
test3();
}
}

有返回值的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Main {
public static void main(String[] args) {
}

public int test() {
try {
return test1();
} catch (Exception e) {
return test2();
} finally {
return test3();
}
}

private int test1() {
return 1;
}

private int test2() {
return 2;
}

private int test3() {
return 3;
}
}

运行结果为3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int test();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=4, args_size=1
0: aload_0
1: invokespecial #2 // Method test1:()I
4: istore_1
5: aload_0
6: invokespecial #3 // Method test3:()I
9: ireturn

10: astore_1
11: aload_0
12: invokespecial #5 // Method test2:()I
15: istore_2
16: aload_0
17: invokespecial #3 // Method test3:()I
20: ireturn

21: astore_3
22: aload_0
23: invokespecial #3 // Method test3:()I
26: ireturn

从字节码上看就能知道, finally 在任何情况下都会执行, 所以return test3();必定会被执行, 所以一定返回了3.

另外一种有返回值的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) {
Main m = new Main();
System.out.println(m.test());
}

public int test() {
int i = 0;
try {
// int err = 1 / 0;
// 其他代码
return i;
} catch (Exception e) {
// 其他代码
return i;
} finally {
i = ++i;
}
}
}

结果为:0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public int test();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: istore_2
4: iinc 1, 1
7: iload_1
8: istore_1
9: iload_2
10: ireturn

11: astore_2
12: iload_1
13: istore_3
14: iinc 1, 1
17: iload_1
18: istore_1
19: iload_3
20: ireturn

21: astore 4
23: iinc 1, 1
26: iload_1
27: istore_1
28: aload 4
30: athrow

为什么finally一定会执行, 但是结果缺不是i = i + 1只后的1, 而是0呢? 从字节码上同样可以看出原因.

  1. 局部变量表第一个位置是this (this,null,null,exception)
  2. 局部变量表第二个位置写入0 (this,0,null,exception)
  3. 加载局部变量表第二个位置的值到操作数栈, [0]
  4. 写入局部变量表第三个位置的值 (this,0,0,exception)
  5. 局部变量表第二个位置值加一 (this,1,0,exception)
  6. 加载局部变量表第二个位置的值到操作数栈, [1]
  7. 写入局部变量表第二个位置 (this,1,0,exception)
  8. 加载局部变量表第三个位置的值到操作数栈 [0]
  9. 返回操作数栈内的值 0

翻译成伪代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
int i = 0;
try {
// 其他代码
int temp = i;
i = ++i;
return temp;
} catch (Exception e) {
// 其他代码
int temp = i;
i = ++i;
return temp;
}

returnfinally{}的情况下, return返回的值会有一个tmp保存, 等finally{}执行完成后, 会返回这个tmp.

发现LocalVariableTable里面显示的不全, locals=5 但是LocalVariableTable里面只有3条, thistmp没有显示出来.(也有可能是我理解的有问题)

finally中抛出异常

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
try {
throw new RuntimeException("try");
} catch (Exception e) {
throw new RuntimeException("catch");
} finally {
throw new RuntimeException("finally");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0: new           #2                  // class java/lang/RuntimeException
3: dup
4: ldc #3 // String try
6: invokespecial #4 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
9: athrow
10: astore_1
11: new #2 // class java/lang/RuntimeException
14: dup
15: ldc #6 // String catch
17: invokespecial #4 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
20: athrow
21: astore_2
22: new #2 // class java/lang/RuntimeException
25: dup
26: ldc #7 // String finally
28: invokespecial #4 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
31: athrow

无论如何都会抛出finally中抛出的异常, 导致某些异常无法捕捉到, 可以使用addSuppressed(e), 把未被打出的异常信息记录起来, 写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) {
Exception tmpException = null;
try {
throw new RuntimeException("try");
} catch (Exception e) {
tmpException = e;
throw new RuntimeException("catch", e);
} finally {
try {
throw new RuntimeException("finally");
} catch (Exception e) {
tmpException.addSuppressed(e);
}
}
}
}

异常打印结果:

1
2
3
4
5
6
Exception in thread "main" java.lang.RuntimeException: catch
at com.yuda.test.Main.main(Main.java:15)
Caused by: java.lang.RuntimeException: try
at com.yuda.test.Main.main(Main.java:12)
Suppressed: java.lang.RuntimeException: finally
at com.yuda.test.Main.main(Main.java:18)

线程同步

方法上的synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
test();
}

private static synchronized void test(){
System.out.println(1);
}
}

private static synchronized void test();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_1
4: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
7: return
LineNumberTable:
line 14: 0
line 15: 7

flags来标记方法为ACC_SYNCHRONIZED, 进入方法时, JVM会尝试获取锁, 如果是实例方法, 获取对象锁, 如果是类方法, 则获取类锁.

synchronized{} 代码块

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
}
private void test() {
synchronized (Main.class) {
System.out.println(1);
}
}
}

生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void test();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/yuda/test/Main
2: dup
3: astore_1

4: monitorenter

5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: iconst_1
9: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
12: aload_1
13: monitorexit
14: goto 22

17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
5 14 17 any
17 20 17 any
  1. com/yuda/test/Main压入栈
  2. 复制一个栈顶元素
  3. 放到局部变量表的2位置
  4. monitorenter指令, 用栈顶元素做锁.
  5. 块内打印逻辑执行
  6. 从局部变量表的2位置取出, 压入栈
  7. monitorexit指令, 用栈顶元素释放锁
  8. 跳转到return.

为了保证锁一定能释放, JVM自动对synchronized{}加了异常处理的逻辑(即finally{})

牛逼的文章: Synchronized解析——如果你愿意一层一层剥开我的心