Code 属性表是一种用来存储方法体的属性表。它包含了方法的指令、局部变量表、操作数栈、异常处理器等信息。
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: 指向常量池中 CONSTANT_Utf8 类型的常量, 固定为"Code"
- attribute_length: 属性值的长度
- max_stack: 栈帧中操作数栈的最大深度。在方法执行的任意时刻, 操作数栈都不会超过这个深度。JVM 运行的时候需要根据这个值来分配栈帧中的操作数栈深度
- max_locals: 局部变量表所需的存储空间
- code_length: 字节码指令的长度
- code: 用于存储字节码指令。每个指令是一个 u1 类型的单字节, 当 JVM 读取到 code 中的一个字节码时, 就可以找出这个字节码代表的是什么指令, 并且可以知道这条指令后面是否需要跟随参数, 以及后续的参数应当如何解析。一个 u1 类型一共可以表达 256 条指令, 目前已经定义了大约 200 条指令
- exception_table_length: 显式异常处理表的长度
- exception_table: 显式异常处理表, 用来实现 try-catch-finally, 显式异常处理表对于 Code 属性来说并不是必须存在的
- attributes_count 和 attribute_info 用来存储 Code 属性的附加属性
以下面代码为例, 分析一下Code 属性表:
public class App {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
System.out.println(c);
}
}
看一下字节码文件中 main 方法的 Code 属性表的内容:
常量池如下:
attribute_name_index和attribute_length
attribute_name_index 为 0x0008, 指向常量池中索引为 8 的值"Code"。attribute_length 为 0x00000068, 转换成十进制是 104。
max_stack和max_locals
max_stack 的值为 0x0002, max_locals 的值是 0x0004。这里简单介绍一下 Java 的栈帧: 每个方法被执行的时候, JVM 都会在 Java 栈中创建一个栈帧用于存储局部变量表、操作数栈等方法需要用到的信息。局部变量表是一个数组, 用于存放方法的参数和方法内部定义的局部变量, JVM 通过索引访问局部变量表中的数据。操作数栈主要用于保存计算过程的中间结果。例如上面代码中的 int c = a + b 的加法运算就会用到操作数栈。max_stack 是这个方法执行过程中操作数栈的最大深度, max_locals 是这个方法的局部变量表的最大容量。
code_length和code
code_length 为 0x000000010, 所以接下来的 16 个字节就是 main 方法的字节码指令:
- 0x04 是字节码指令 iconst_1, 它的作用是把一个 int 类型值 1 入栈(操作数栈)
- 0x3C 是字节码指令 istore_1, 它的作用是把栈顶的 int 值存入局部变量表中索引为 1 的位置, 对应 Java 代码的 int a = 1
- 0x05 是字节码指令 istore_2, 它的作用是把一个 int 类型值 2 入栈
- 0x3D 是字节码指令 istore_2, 它的作用是把栈顶的 int 值存入局部变量表中索引为 2 的位置, 对应 Java 代码的 int b = 2
- 0x1B 是字节码指令 iload_1, 它的作用是把局部变量表中索引为 1 的 int 变量入栈
- 0x1C 是字节码指令 iload_2, 它的作用是把局部变量表中索引为 2 的 int 变量入栈, iload_1, iload_2 两条指令都是为了下面的加法指令做准备工作
- 0x60 是字节码指令 iadd, 它的作用是把栈顶的两个 int 值取出并相加, 然后把计算结果入栈
- 0x3E 是字节码指令 istore_3, 它的作用是把栈顶的 int 值存入局部变量表中索引为 3 的位置, iadd 和 istore_3 两条指令共同实现了 Java 代码的 int c = a + b
- 0xB2 是字节码指令 getstatic, 它的作用是获取指定类的静态字段, 并把它入栈。后面的 0x0002 是它的参数, 指向常量池中索引为 2 的常量: java.lang.System 类的 out 字段, 它是一个 PrintStream 类型的对象
- 0x1D 是字节码指令 iload_3, 它的作用是把局部变量表中索引为 3 的 int 变量入栈, getstatic, iload_3 两条指令都是为了下面的方法调用指令做准备工作
- 0xB6 是字节码指令 invokevirtual, 它的作用是调用某个对象的方法。后面的 0x0003 是它的参数, 指向常量池中索引为 3 的常量: java.io.PrintStream 类的 println 方法。println 方法的调用者是 getstatic 入栈的 System.out 对象, 方法的参数是 iload_3 入栈的 int 值 3
- 0xB1 是字节码指令 return, 它的作用是从当前方法返回 void
exception_table_length和exception_table
exception_table_length 为 0x0000, main 方法没有手动抛出异常, 所以显式异常处理表为空。
attributes_count和attributes
attributes_count 为 0x0002, 表示 Code 属性表包含了两个附加属性(LineNumberTable 和 LocalVariableTable 两个属性)。
使用 javap -verbose App.class 命令可以更直观地看到 main 方法的内容: