引导
Class文件的基本结构
Class文件的常量池
Class文件的访问标志,类索引,父类索引,接口索引
Class文件的字段和方法
Class文件中的字段
在 Class文件的基本结构 一文中,简单的介绍了 class 文件中的字段,它是由字段计数器(field_count)和字段表(fields)两个部分组成,排列在接口表(interfaces)的后面,如下图红色框所示。
你知道在 java 中字段由哪几个部分组成的吗?这里我绘制了 java 中字段的组成图,如下所示。
- 访问权限即
private
、protected
、public
和包访问权限; -
static
表示是否是静态; -
final
表示是否可被修改; -
volatile
即并发可见性; -
transient
即是否序列化;
JVM 规定了用 Field_info
结构体来描述字段,它的结构如下图所示。
-
access_flags
为字段的访问标志,比如前面所说的访问权限,是否静态等都会被 java 编译器编译成的access_flags
; -
name_index
和descriptor_index
是一个指向常量池(constant_pool)数组的有效索引值; -
attributes_count
描述了字段的属性个数; -
attributes
是一个由ConstantValue
结构体组成的数组,数组长度为attributes_count
。
字段的访问标志(access_flags)占用 2 个字节,每一位表示的信息如下图所示。
举个栗子。定义如下代码。
public class ClassFile {
private static final transient String test = "hello";
}
按照上面的描述,test
字段在 class 文件中的访问标志(access_flags)应该为 ACC_PRIVATE
、ACC_STATIC
、ACC_FINAL
和ACC_TRANSIENT
。其编译成的 class 文件如下图所示,和猜想一致。
Class文件中的方法
在 Class文件的基本结构 一文中,简单的介绍了 class 文件中的方法,它是由方法计数器(method_count)和方法表(methods)两个部分组成,排列在字段表(fields)的后面,如下图红色框所示。
你知道在 java 中方法由哪几个部分组成的吗?这里我绘制了 java 中方法的组成图,如下所示。
- 访问权限即
private
、protected
、public
和包访问权限; -
static
表示是否是静态; -
final
表示是否可被修改; -
synchronized
是否是同步方法; -
native
是否是native
方法; -
abstract
是否是抽象方法; - 方法描述包括方法参数和方法返回值类型;
JVM 规定了用 Method_info
结构体来描述方法,它的结构如下图所示。
访问标志
访问标志(access_flags)占用 2 个字节,每一位表示的信息如下图所示。
举个栗子。定义如下代码。
public class ClassFile {
public static synchronized final void test() {
}
}
按照上面所讲,test()
在 class 文件中存储的访问标志(access_flags) 应该是 ACC_PUBLIC
、ACC_STATIC
、ACC_SYNCHRONIZED
和 ACC_FINAL
。下图为 ClassFile
生成的 class 文件,和我们的猜想一致。
名称索引
name_index
是一个指向常量池(constant_pool)数组的有效索引值,且此索引处的常量池项是 CONSTANT_Utf8_info
结构,描述了方法的名称。
描述符索引
descriptor_index
是一个指向常量池(constant_pool) 数组的有效索引值,且此索引处的常量池项是 CONSTANT_Utf8_info
结构,描述了方法的返回值类型和参数类型。
方法描述符 = (方法参数类型描述符列表)方法返回值类型描述符
举个栗子。定义如下代码。
public class ClassFile {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
根据上面所说,我们猜想如下:
-
getName()
方法在 class 文件中对应的方法描述符为()Ljava/lang/String;
; -
setName()
方法在 class 文件中对应的方法描述符为(Ljava/lang/String;)V
。
ClassFile
对应的 class 文件如下图所示,和我们的猜想一样。
属性
属性记录了方法的一些属性信息。这些信息包括:
- 方法的代码实现,即机器指令;
- 方法声明的异常信息;
- 方法是否被标记为过时
@deprecated
; - 方法是否是编译器自动生成。
attribute_info
的结构如下图所示。
attribute_name_index
是一个指向常量池(constant_pool)数组的有效索引值,表示该 attribute_info
项表示的是哪一种类型的属性。属性名及其具体含义如下图所示。
attribute_length
表示属性值用多少个字节来进行存放;
attribute_value
表示存放的属性信息。
Code类型的属性
Code
类型的属性是方法最为重要的属性部分。因为它包含了 JVM 运行的机器码指令。
Code
属性一般由如下 4 个部分组成:
- 机器指令(Code);
- 异常处理跳转信息(ExceptionTable),如果代码中出现了
try{}catch{}
块,那么try{}
块内的机器指令的地址范围记录下来,并且记录对应的catch{}
块中的起始机器指令地址,当运行时在try{}
块中有异常抛出的话,JVM 会将 `catch{} 块对应的起始机器指令地址传递给PC寄存器,从而实现指令跳转; - java 源文件行号和机器指令的对应关系(LineNumberTable);
- 局部变量描述信息(LocalVariableTable),它会记录栈帧局部变量表中的变量和 java 源文件中定义的变量之间的关系,这个信息不是运行时必须的属性,默认情况下不会生成到 class 文件中。
局部变量描述信息有什么作用?我们在开发的时候经常会用到代码自动补全功能,而有的时候会发现调用方法的参数都是 p0,p1 这种没有实际意义的符号,这其实就是没有局部变量描述信息所导致的。
Code
属性的基本结构如下图所示。
-
attribute_name_index
是一个指向常量池(constant_pool)数组的一个有效索引值,且常量池(constant_pool)数组在此索引出的项为代表Code
的Constant_Utf8_info
结构; -
attribute_length
表示属性值占用的长度; -
max_stack
表示操作数栈深度的最大值,在方法执行的任意时刻,操作数栈都不应该超过这个值,JVM 的运行的时候,会根据这个值来设置该方法对应的栈帧(Stack Frame)中的操作数栈的深度; -
max_locals
表示最大局部变量数,表示局部变量表所需要的存储空间大小; -
code_length
表示机器指令长度,即跟在其后的多少个字节是机器指令; -
code
机器指令区域。JVM 最底层的要执行的机器指令就存储在这里; -
exception_table_length
显式异常表长度,如果在方法代码中出现了try{} catch()
形式的结构,该值不会为空,紧跟其后会跟着若干个exception_table
结构体,以表示异常捕获情况; -
exception_table
显式异常表,start_pc
,end_pc
,handler_pc
中的值都表示的是PC计数器中的指令地址。exception_table
表示的意思是:如果字节码从第start_pc
行到第end_pc
行之间出现了catch_type
所描述的异常类型,那么将跳转到handler_pc
行继续处理; -
attribute_count
属性计数器,表示Code
属性表的其他属性的数目; -
attribute_info
表示Code
属性具有的属性,它主要分为三个类型的属性表:LineNumberTable
类型、LocalVariableTable
类型。
LineNumberTable
记录着 java 源文件和机器指令之间的对应关系;
LocalVariableTable
记录着局部变量描述。
举个栗子。定义如下代码。
public class ClassFile {
public void test() {
String test = "hello";
try {
byte[] bytes = test.getBytes("utf-8");
} catch(Exception e) {
}
}
}
它对应的 class 文件中的 Code
属性如下图所示。