在class文件结构解析一文中,我们介绍了class文件的构成,整个class文件一共包含3部分共16个属性:
- 3个描述文件属性的数据项:魔数和主次版本号
- 11个描述类属性的数据项:类、字段、方法等信息
- 2个描述代码属性的数据项:属性表,描述方法体内的具体内容
其中文件属性和类属性在上一篇中已经有过介绍,本文将主要介绍一下属性表。在最新的JVM规范中,一共定义了21个属性,接下来我将对其中一些关键属性进行分析。
对于每一个属性,它的结构可以分为3部分:
- 一个u2类型的属性名称(attribute_name_index):从常量池中引用的一个常量
- 一个u4类型的属性长度(attribute_length):属性值所占用的字节数
- attribute_length个u1类型的属性值:具体的属性值
1. Code属性
java源文件方法体中的代码经过编译后,最终存储在Code属性内,它的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
- attribute_name_index是一个指向常量池中某一个常量的索引,取值固定为Code
- attribute_length表示属性值的长度
- max_stack表示操作数栈的最大深度,jvm运行时会根据这个值来分配栈帧中的操作数栈深度
- max_locals表示局部变量表所需要的存储空间,单位为slot
- code_length代表字节码指令长度
- code代表具体的字节码指令,根据jvm规范,每个字节码指令占用一个字节,jvm可以自动识别该指令是否需要接收参数。
- exception_table_length表示异常表占用的字节数
- exception_table表示具体的异常表
- Code属性本身还有自己的一些属性表,包括LineNumberTable、LocalVariableTable和StackMapTable,这些属性不是必须的,如果有的话,会在attributes_count和attributes中体现出来
2. LineNumberTable属性
LineNumberTable是Code属性中的一个子属性,用来描述java源文件行号与字节码文件偏移量之间的对应关系。当程序运行抛出异常时,异常堆栈中显示出错的行号就是根据这个对应关系来显示的,它的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
其中line_number_info结构如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | start_pc | 1 | 字节码偏移量 |
u2 | line_number | 1 | java源文件行号 |
3. LocalVariableTable属性
LocalVariableTable也是Code属性中的一个子属性,用来描述栈帧的局部变量表中变量与java源码中变量的对应关系,其结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | llocal_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
其中local_variable_info结构如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | start_pc | 1 | 变量生命周期开始时的字节码偏移量 |
u2 | length | 1 | 变量作用范围覆盖的字节数 |
u2 | name_index | 1 | 索引值,指向变量名称 |
u2 | descriptor_index | 1 | 索引值,指向变量描述符 |
u2 | index | 1 | 变量在栈帧中slot的位置 |
LineNumberTable和LocalVariableTable都不是运行时必须的,可以在javac中使用-g:none选项来取消生成这两个属性,取消前后反编译出来的文件将丢失这两个属性,如下图所示:
4. 实例分析
我们还是继续以上一篇中的代码为例进行分析:
java源文件:
利用javap得到的字节码内容(这里只给出了main方法的):
// 这一部分是描述方法的元数据,在上一篇已经分析过
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
// 这一部分描述方法体的具体内容
Code:
// 操作数栈最大深度为3,局部变量表最多需要一个slot,有一个入参
// 如果是实例方法,还会增加一个this对象的引用作为隐形参数
stack=3, locals=1, args_size=1
// 具体的字节码指令,共占用28个字节
// 获取System.out属性值,压入栈顶
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 创建StringBuilder实例,并将其引用压入栈顶
3: new #3 // class java/lang/StringBuilder
// 复制栈顶元素,并压入栈顶
6: dup
// 调用init()方法,弹出栈顶元素
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
// 加载常量Hello,压入栈顶
10: ldc #5 // String Hello
// 调用StringBuilder的append()方法
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
// 获取name属性值
15: getstatic #7 // Field name:Ljava/lang/String;
// 调用StringBuilder的append()方法
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
// 调用StringBuilder的toString()方法
21: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
// 调用PrintStream的println()方法
24: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
// java源码行号与字节码文件偏移量的对应关系
LineNumberTable:
line 14: 0
line 15: 27
// 栈帧中局部变量表变量与java源码变量的对应关系
LocalVariableTable:
Start Length Slot Name Signature
0 28 0 args [Ljava/lang/String;
再来看看main方法code属性对应的十六进制文件,按照code属性结构对其进行分析,可以发现其内容与javap得到的结果完全一致。
main方法里只看到了加载常量Hello的操作,那么有人可能会问,静态常量name的属性值是在哪里加载的呢?实际上,这一步在cinit()方法中就完成了。
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
// 类方法,不会传入this引用,故args_size=0
stack=1, locals=0, args_size=0
// 从常量池加载静态常量,压入栈顶
0: ldc #10 // String JVM
// 为静态属性name赋值
2: putstatic #7 // Field name:Ljava/lang/String;
5: return
LineNumberTable:
line 11: 0
通过查看方法的字节码,可以更直观的看出方法内部的执行逻辑。比如说对于字符串连接操作:
"Hello " + name;
底层实际上是通过新建一个StringBuilder对象来实现的:
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(name);
sb.toString();
事实上,很多问题都可以由此迎刃而解,比如说i++和++i到底有什么区别。以下4段代码,最终i的值分别是多少呢?稍有经验的程序员都可以轻松给出答案,但是其实现原理是什么呢,我们不妨从字节码角度来略探一二。
public static void incr1() {
int i = 0;
i = i++;
}
public static void incr2() {
int i = 0;
i++;
}
public static void incr3() {
int i = 0;
i = ++i;
}
public static void incr4() {
int i = 0;
++i;
}
public static void incr1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iload_0
3: iinc 0, 1
6: istore_0
7: return
LineNumberTable:
line 20: 0
line 21: 2
line 22: 7
LocalVariableTable:
Start Length Slot Name Signature
2 6 0 i I
public static void incr2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iinc 0, 1
5: return
LineNumberTable:
line 26: 0
line 27: 2
line 28: 5
LocalVariableTable:
Start Length Slot Name Signature
2 4 0 i I
public static void incr3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iinc 0, 1
5: iload_0
6: istore_0
7: return
LineNumberTable:
line 32: 0
line 33: 2
line 34: 7
LocalVariableTable:
Start Length Slot Name Signature
2 6 0 i I
public static void incr4();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iinc 0, 1
5: return
LineNumberTable:
line 38: 0
line 39: 2
line 40: 5
LocalVariableTable:
Start Length Slot Name Signature
2 4 0 i I
具体分析可以参考占小狼的文章从字节码角度分析 i++ 和 ++i 实现,图文并茂,清晰易懂。