Java字节码的详细讲解-刘宇
作者:刘宇
CSDN博客地址:https://blog.csdn.net/liuyu973971883
有部分资料参考,如有侵权,请联系删除。如有不正确的地方,烦请指正,谢谢。
一、字节码的整体结构
- 使用javap -verbose命令分析一个字节码文件时,将会分析该字节码的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息。
- 如果想要解析字节码中的私有方法必须加上-p参数:javap -verbose -p class文件
- 也可通过jclassilb这个插件来查看字节码文件
整体结构:
整体结构的拆分表:
字节码中的两种数据类型:
- 字节数据直接量:这是基本的数据类型。共细分为U1、U2、U4、U8四种,分别代表练习的1个字节、2个字节、4个字节、8个字节组成的整体数据。
- 表(数组):表是由多个基本数据或其他表,按照既定的顺序组成的大的数据集合。表是有结构的,它的结构体现在:组成表的成分所在位置和顺序都是已经严格定义好的。
二、字节码范围解析
2.1、魔数
占据第1~4个字节:所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE。
2.2、版本信息
占据第5~8字节:前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号)。这里的版本号为00 00 00 34,换算成十进制,表示此版本号为0,主版本号为52。所以该文件的版本号为1.8.0。可以通过java -version来验证
2.3、常量池(constant pool)
紧接着主版本号之后的就是常量池入口。一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看做是Class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存储在常量池中的。常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,Java中申明为final的常量值等,而符号引用如类和接口的全局限定名(包名与类名一起的),字段的名称和描述符,方法的名称和描述符等。常量池并非只能存储常量,也可以存储变量
总体结构:
Java类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同组成。
常量池数量:
紧跟着主版本号后面,占据2个字节;常量池数组则紧跟常量池数量之后。
常量池数组:
常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同,但是每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据1个字节,一共有四种标志位U1、U2、U4、U8分别占1、2、4、8个字节。JVM在解析常量池时,会根据就这个u1类型来获取元素的具体类型。值得注意的是,常量池数组中元素的个数=常量池数量-1(其中0暂时不适应),目的是满足某些常量池索引值的数据在特定情况下需要表达【不引用任何一个常量池】的含义;根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值;所以,常量池的所有从1而非0开始。
常量池中11种数据类型的结构表:
2.4、描述符规则
在JVM中,每个变量/字段都有描述信息,主要用于描述字段的数据类型、方法的参数类别与返回值。如:B – byte,C – char,D – double,F – float,I – int,J – long,S – short,Z – boolean,V – void,L – 对象类型(如Ljava/lang/String),数组 – 每一个维度前置使用[表示。描述方法时描述顺序按照:先参数列表,后返回值的顺序
2.5、访问标志(Access Flag)
访问标志信息占据2个字节,它包括该Class文件是类还是结构,是否被定义成public,是否是abstract,如果是类,是否被声明成final。在字节码中它是并集显示的,如两个字节00 21即:ACC_PUBLIC和ACC_SUPER的并集。
访问标志符分类:
2.6、类名(This Class)
描述该类的名称在常量池中的索引,占据2个字节。
2.7、父类名(Super Class)
描述该类的父类的名称在常量池中的索引,占据2个字节。
2.8、接口(Interfaces)
描述该类实现了哪些接口,占据2+n个字节,它分为两部分:interface_count(接口个数)2个字节、interfaces(接口名)每个接口名占据2个字节。
注:当我们interface_count为0时则字节码为00 00时,后面一部分(即:interfaces接口名)就不会出现在字节码文件中。
2.9、成员变量(Fields)
描述类和接口中声明了哪些变量,包含了类级别的变量以及实例变量,但是不包括方法内部声明的局部变量。它占据2+n个字节,它分为两部分:field_count(成员变量个数)2个字节、fields(变量表)n个字节。
变量表的结构:
类型 | 名称 | 数量 |
---|---|---|
U2 | access_flags(访问标志符) | 1 |
U2 | name_index(名称在常量池的索引) | 1 |
U2 | descriptor_index(类型描述符在常量池的索引) | 1 |
U2 | attributes_count(属性数量) | 1 |
attribute_info | attributes(属性信息表,属性数为0时则不存在) | attributes_count |
field_info{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
2.10、方法(Methods)
描述类中包含的方法。它占据2+n个字节,它分为两部分:method_count(方法个数)2个字节、methods(方法表)n个字节。
方法表的结构:
类型 | 名称 | 数量 |
---|---|---|
U2 | access_flags(访问标志符) | 1 |
U2 | name_index(名称在常量池的索引) | 1 |
U2 | descriptor_index(类型描述符在常量池的索引) | 1 |
U2 | attributes_count(属性数量) | 1 |
attribute_info | attributes(属性信息表,属性数为0时则不存在) | attributes_count |
method_info{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
2.11、属性表(attribute_info)
- 用于存放变量以及方法的属性,如:方法中的代码就存储在Code属性表中
- JVM预定义了部分attribute,但是编译器字节也可以实现字节的attribute写如Class文件里,供运行时使用。
- 不同的attribute通过attribute_name_index来区分,如我们所知的方法中的属性表(Code属性)、Code属性中的属性表(LineNumberTable属性、LocalVariableTable属性)、字节码文件中的属性表(SourceFile属性)
属性表的结构:
类型 | 名称 | 数量 |
---|---|---|
U2 | attribute_name_index(属性名索引) | 1 |
U4 | attribute_length(属性长度) | 1 |
U1 | info(信息表) | attribute_length |
attribute_info{
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
2.12、方法表中的Code属性
- attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段
- max_stack表示这个方法运行时的任何时刻所能表达的操作数栈的最大深度
- mac_locals表示方法执行期间创建的局部变量数目,包含用来表示传入的参数的局部变量
- code_length表示该方法所包含的字节码的字节数以及具体的指令码,具体字节码即是该方法被调用时,虚拟机所执行的字节码
- exception_table,这里存放的是处理异常的信息,每个exception_table表项由u2 star_pc、end_pc、handler_pc、catch_type组成
- start_pc和end_pc表示在code数组中的从start_pc到end_pc处(含头不含尾)的指令抛出的异常会由这个表项来处理。
- handler_pc表示处理异常的代码的开始处。
- catch_type表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理所有异常。
Code_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 star_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
}exception_table[exception_table_length];
u2 attribute_count;
attribute_info attribute[attribute_count]
}
2.13、Code属性中的LineNumberTable属性
- attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段
- line_number_table_length表示行号表的总数
- star_pc表示助记词的行号
- line_number表示源代码的行号
LineNumberTable_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 star_pc;
u2 line_number;
}line_number_table[line_number_table_length];
}
2.14、Code属性中的LocalVariableTable属性
- attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段
- local_variable_table_length表示局部变量的个数
- star_pc表示局部变量的开始位置
- length表示局部变量的结束位置
- index表示局部变量的名称索引
- descriptor_index表示局部变量的描述索引
- index表示StackMapTable,主要用于校验检查
LocalVariableTable_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{
u2 star_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
}local_variable_table[local_variable_table_length];
}
三、synchronize关键字在字节码中的描述
- synchronize关键字在字节码中必定会出现moniterenter以及moniterexit来表示进入/退出锁,并且moniterenter只会有一处,而moniterexit会有多处,包含了程序正常执行结束的moniterexit以及异常退出的moniterexit。
- synchronize的使用方法:1、在实例化方法上使用(表示给当前对象this加锁);2、在静态方法上使用(表示给当前类的所对应的class对象加锁);3、在方法内部使用。
3.1、在实例化方法上使用
- 源代码:
package com.brycen.trucks.byteclass;
public class MyTest2 {
...
private synchronized void setX(int x){
this.x = x;
}
...
}
- 字节码:
由此可以看出synchronized作用于方法上时在code属性中是没有moniterenter、monitexit标识符的,因为synchronized的周期是整个code所以无需显视的显示在code当中了,而是通过访问标志ACC_SYNCHRONIZED来说明
private synchronized void setX(int);
descriptor: (I)V
flags: ACC_PRIVATE, ACC_SYNCHRONIZED
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #4 // Field x:I
5: return
LineNumberTable:
line 27: 0
line 28: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/ideas/trucks/byteclass/MyTest2;
0 6 1 x I
MethodParameters:
Name Flags
x
3.2、在静态方法上使用
- 源代码:
package com.brycen.trucks.byteclass;
public class MyTest2 {
...
private synchronized static void setX(int x){
}
...
}
- 字节码:
由此可以看出它只是比作用于实例方法上多了一个访问标志ACC_STATIC,它实则是给这个方法所属类的Class对象上的锁
private static synchronized void setX(int);
descriptor: (I)V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 27: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 x I
MethodParameters:
Name Flags
x
3.3、在方法内部使用
- 源代码:
package com.brycen.trucks.byteclass;
public class MyTest2 {
...
private void test(String str){
synchronized (str){
System.out.println("hello world");
}
}
...
}
- 字节码:
可以看出,此时在code属性中出现了monitorenter以及monitorexit,并且有多个monitorexit,这是因为程序的健壮性,不光在程序正常结束的时候可以monitorexit,也防止出现异常时可以正常的monitorexit释放锁
private void test(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PRIVATE
Code:
stack=2, locals=4, args_size=2
0: aload_1
1: dup
2: astore_2
3: monitorenter
4: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #11 // String hello world
9: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_2
13: monitorexit
14: goto 22
17: astore_3
18: aload_2
19: monitorexit
20: aload_3
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 30: 0
line 31: 4
line 32: 12
line 33: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 this Lcom/ideas/trucks/byteclass/MyTest2;
0 23 1 str Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class com/brycen/trucks/byteclass/MyTest2, class java/lang/String, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
MethodParameters:
Name Flags
str
四、静态变量和变量的赋值时机
- 在我们类的构造方法<init>内并没有任何代码,而在字节码中,我们可以看到,它却生成了很多代码。这是因为构造方法会对我们类的变量进行赋值操作,而这些操作都会在构造方法里完成。
- 下面的static代码块实则是<clinit>,它主要是处理静态代码块中的代码以及静态变量赋值的操作
...
public com.ideas.trucks.byteclass.MyTest2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String Welcome
7: putfield #3 // Field str:Ljava/lang/String;
10: aload_0
11: iconst_5
12: putfield #4 // Field x:I
15: return
LineNumberTable:
line 10: 0
line 12: 4
line 14: 10
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lcom/ideas/trucks/byteclass/MyTest2;
...
...
...
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: invokestatic #8 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #9 // Field in:Ljava/lang/Integer;
8: return
LineNumberTable:
line 16: 0
...
五、分析在字节码中的this、异常表
源代码
package com.brycen.demo.byteclass;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
public class MyTest3 {
public void test(){
try{
InputStream in = new FileInputStream("text.txt");
ServerSocket serverSocket = new ServerSocket(9999);
serverSocket.accept();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("finally");
}
}
}
字节码
Classfile /Users/liuyu/IdeaProjects/demo/target/classes/com/brycen/demo/byteclass/MyTest3.class
Last modified 2021-9-25; size 1276 bytes
MD5 checksum f06777dfe6bff8db829d9a0650ee19b2
Compiled from "MyTest3.java"
public class com.brycen.demo.byteclass.MyTest3
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #18.#42 // java/lang/Object."<init>":()V
#2 = Class #43 // java/io/FileInputStream
#3 = String #44 // text.txt
#4 = Methodref #2.#45 // java/io/FileInputStream."<init>":(Ljava/lang/String;)V
#5 = Class #46 // java/net/ServerSocket
#6 = Methodref #5.#47 // java/net/ServerSocket."<init>":(I)V
#7 = Methodref #5.#48 // java/net/ServerSocket.accept:()Ljava/net/Socket;
#8 = Fieldref #49.#50 // java/lang/System.out:Ljava/io/PrintStream;
#9 = String #51 // finally
#10 = Methodref #52.#53 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = Class #54 // java/io/FileNotFoundException
#12 = Methodref #11.#55 // java/io/FileNotFoundException.printStackTrace:()V
#13 = Class #56 // java/io/IOException
#14 = Methodref #13.#55 // java/io/IOException.printStackTrace:()V
#15 = Class #57 // java/lang/Exception
#16 = Methodref #15.#55 // java/lang/Exception.printStackTrace:()V
#17 = Class #58 // com/brycen/demo/byteclass/MyTest3
#18 = Class #59 // java/lang/Object
#19 = Utf8 <init>
#20 = Utf8 ()V
#21 = Utf8 Code
#22 = Utf8 LineNumberTable
#23 = Utf8 LocalVariableTable
#24 = Utf8 this
#25 = Utf8 Lcom/brycen/demo/byteclass/MyTest3;
#26 = Utf8 test
#27 = Utf8 in
#28 = Utf8 Ljava/io/InputStream;
#29 = Utf8 serverSocket
#30 = Utf8 Ljava/net/ServerSocket;
#31 = Utf8 e
#32 = Utf8 Ljava/io/FileNotFoundException;
#33 = Utf8 Ljava/io/IOException;
#34 = Utf8 Ljava/lang/Exception;
#35 = Utf8 StackMapTable
#36 = Class #54 // java/io/FileNotFoundException
#37 = Class #56 // java/io/IOException
#38 = Class #57 // java/lang/Exception
#39 = Class #60 // java/lang/Throwable
#40 = Utf8 SourceFile
#41 = Utf8 MyTest3.java
#42 = NameAndType #19:#20 // "<init>":()V
#43 = Utf8 java/io/FileInputStream
#44 = Utf8 text.txt
#45 = NameAndType #19:#61 // "<init>":(Ljava/lang/String;)V
#46 = Utf8 java/net/ServerSocket
#47 = NameAndType #19:#62 // "<init>":(I)V
#48 = NameAndType #63:#64 // accept:()Ljava/net/Socket;
#49 = Class #65 // java/lang/System
#50 = NameAndType #66:#67 // out:Ljava/io/PrintStream;
#51 = Utf8 finally
#52 = Class #68 // java/io/PrintStream
#53 = NameAndType #69:#61 // println:(Ljava/lang/String;)V
#54 = Utf8 java/io/FileNotFoundException
#55 = NameAndType #70:#20 // printStackTrace:()V
#56 = Utf8 java/io/IOException
#57 = Utf8 java/lang/Exception
#58 = Utf8 com/brycen/demo/byteclass/MyTest3
#59 = Utf8 java/lang/Object
#60 = Utf8 java/lang/Throwable
#61 = Utf8 (Ljava/lang/String;)V
#62 = Utf8 (I)V
#63 = Utf8 accept
#64 = Utf8 ()Ljava/net/Socket;
#65 = Utf8 java/lang/System
#66 = Utf8 out
#67 = Utf8 Ljava/io/PrintStream;
#68 = Utf8 java/io/PrintStream
#69 = Utf8 println
#70 = Utf8 printStackTrace
{
public com.brycen.demo.byteclass.MyTest3();
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
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/brycen/demo/byteclass/MyTest3;
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=1
0: new #2 // class java/io/FileInputStream
3: dup
4: ldc #3 // String text.txt
6: invokespecial #4 // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
9: astore_1
10: new #5 // class java/net/ServerSocket
13: dup
14: sipush 9999
17: invokespecial #6 // Method java/net/ServerSocket."<init>":(I)V
20: astore_2
21: aload_2
22: invokevirtual #7 // Method java/net/ServerSocket.accept:()Ljava/net/Socket;
25: pop
26: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
29: ldc #9 // String finally
31: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
34: goto 96
37: astore_1
38: aload_1
39: invokevirtual #12 // Method java/io/FileNotFoundException.printStackTrace:()V
42: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
45: ldc #9 // String finally
47: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
50: goto 96
53: astore_1
54: aload_1
55: invokevirtual #14 // Method java/io/IOException.printStackTrace:()V
58: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
61: ldc #9 // String finally
63: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: goto 96
69: astore_1
70: aload_1
71: invokevirtual #16 // Method java/lang/Exception.printStackTrace:()V
74: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
77: ldc #9 // String finally
79: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
82: goto 96
85: astore_3
86: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
89: ldc #9 // String finally
91: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
94: aload_3
95: athrow
96: return
Exception table:
from to target type
0 26 37 Class java/io/FileNotFoundException
0 26 53 Class java/io/IOException
0 26 69 Class java/lang/Exception
0 26 85 any
37 42 85 any
53 58 85 any
69 74 85 any
LineNumberTable:
line 13: 0
line 15: 10
line 16: 21
line 24: 26
line 25: 34
line 17: 37
line 18: 38
line 24: 42
line 25: 50
line 19: 53
line 20: 54
line 24: 58
line 25: 66
line 21: 69
line 22: 70
line 24: 74
line 25: 82
line 24: 85
line 25: 94
line 26: 96
LocalVariableTable:
Start Length Slot Name Signature
10 16 1 in Ljava/io/InputStream;
21 5 2 serverSocket Ljava/net/ServerSocket;
38 4 1 e Ljava/io/FileNotFoundException;
54 4 1 e Ljava/io/IOException;
70 4 1 e Ljava/lang/Exception;
0 97 0 this Lcom/brycen/demo/byteclass/MyTest3;
StackMapTable: number_of_entries = 5
frame_type = 101 /* same_locals_1_stack_item */
stack = [ class java/io/FileNotFoundException ]
frame_type = 79 /* same_locals_1_stack_item */
stack = [ class java/io/IOException ]
frame_type = 79 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 79 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 10 /* same */
}
SourceFile: "MyTest3.java"
5.1、实例方法中this的分析
疑问:
为什么在test方法中字节码中的args_size=1,很明显我们并没有传入参数呀?
解答:
对于Java类中的每一个实例方法(非static方法),在其编译后所生成的字节码中,方法参数的数量总是会比源代码中的方法参数多一个(this),它位于方法的第一个参数位置处。这个操作是在编译期间完成的,即由javac编译器在编译阶段自动向实例方法传入该this参数。所以,在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量。
5.2、字节码中test方法中的locals为什么是4个?
从源代码中我们可以看到的局部变量是is、serverSocket和三个ex变量,再加上隐藏的this局部变量应该是6个才对呀,为什么是4个呢?这是因为在三个catch中的,程序最多只会走到一个catch中去。
5.3、字节码中异常表的分析
test方法的code属性表:
Code:
stack=3, locals=4, args_size=1
0: new #2 // class java/io/FileInputStream
3: dup
4: ldc #3 // String text.txt
6: invokespecial #4 // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
9: astore_1
10: new #5 // class java/net/ServerSocket
13: dup
14: sipush 9999
17: invokespecial #6 // Method java/net/ServerSocket."<init>":(I)V
20: astore_2
21: aload_2
22: invokevirtual #7 // Method java/net/ServerSocket.accept:()Ljava/net/Socket;
25: pop
26: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
29: ldc #9 // String finally
31: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
34: goto 96
37: astore_1
38: aload_1
39: invokevirtual #12 // Method java/io/FileNotFoundException.printStackTrace:()V
42: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
45: ldc #9 // String finally
47: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
50: goto 96
53: astore_1
54: aload_1
55: invokevirtual #14 // Method java/io/IOException.printStackTrace:()V
58: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
61: ldc #9 // String finally
63: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: goto 96
69: astore_1
70: aload_1
71: invokevirtual #16 // Method java/lang/Exception.printStackTrace:()V
74: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
77: ldc #9 // String finally
79: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
82: goto 96
85: astore_3
86: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
89: ldc #9 // String finally
91: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
94: aload_3
95: athrow
96: return
test方法的异常表:
Exception table:
from to target type
0 26 37 Class java/io/FileNotFoundException
0 26 53 Class java/io/IOException
0 26 69 Class java/lang/Exception
0 26 85 any
37 42 85 any
53 58 85 any
69 74 85 any
异常表分析:
我们从异常表中可以看到,助记符的第0行到第25行发生不同异常时都跳转到相应的catch中去,而后面的any则是处理以上三个catch不能处理的异常的以及catch中出现的异常,虽然在程序角度上Exception可以处理所有异常,但是在字节码中它并不这样任务。
程序处理异常的流程:
- 如果代码中发生FileNotFoundException异常则程序会跳转第37行至50行的catch中,这里我们发现finally中的执行代码也会在其中,然后执行catch中的逻辑随后会goto到96行(即return)
- 如果代码中发生IOException异常则程序会跳转第53行至66行的catch中,这里我们发现finally中的执行代码也会在其中,然后执行catch中的逻辑随后会goto到96行(即return)
- 如果代码中发生Exception异常则程序会跳转第69行至82行的catch中,这里我们发现finally中的执行代码也会在其中,然后执行catch中的逻辑随后会goto到96行(即return)
由上面的流程我们可以得知,finally中的代码在编译成字节码的时候会在每个catch执行范围以及正常程序的执行范围都会存在。
注:如果我们在方法上抛出异常,则在字节码中,该方法的code属性中的异常表不会包含该异常。编译器会在与code属性的同一级别中创建一个Exception属性,将抛出的异常表存储在里面