文章目录
一、概述
Class文件是一组以8位字节为单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件中,中间没有任何分隔符。 当遇到需要占用8位以上的数据项时,则会按照高位在前、低位在后 (Big-endian顺序) 的方式来分割成多个8位字节进行存储。
根据Java虚拟机规范的规定,Class文件格式采用一种类似C语言结构体的伪结构(ClassFile)来存储数据。这种伪结构中只有两种数据类型:无符号数
、表
。
- 无符号数:属于基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值、字符串值。
- 表:由多个无符号数或者子表作为数据项构成的符合数据类型。用于描述有层次关系的复合结构的数据。整个Class其实就是一张表。
注意:
- 任何一个Class文件都对应着唯一的类或接口的定义信息;
- 类或接口并不一定定义在文件里,也可以通过类加载器直接生成。
关联文章:
- 《JVM(一) — Class 文件结构》
- 《JVM(二) — 字节码指令》
- 《JVM(三) — Java虚拟机运行时内存结构》
- 《JVM(四) — 垃圾回收机制》
- 《JVM(五) — 类加载机制》
- 《JVM(六) — JVM面试问题》
- 《JVM — 字节码文件分析》
二、ClassFile 的结构
Class文件格式采用一种类似C语言结构体的伪结构(ClassFile)来存储数据。
链接:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1
ClassFile {
u4 magic; //魔数,固定值0xCAFEBABE
u2 minor_version; //次版本号
u2 major_version; //主版本号
u2 constant_pool_count; //常量个数
cp_info constant_pool[constant_pool_count-1]; //常量池内容
u2 access_flags; //类访问标识
u2 this_class; //当前类索引
u2 super_class; //父类索引
u2 interfaces_count; //接口个数
u2 interfaces[interfaces_count]; //具体的接口内容
u2 fields_count; //字段个数
field_info fields[fields_count]; //具体的字段内容
u2 methods_count; //方法个数
method_info methods[methods_count]; //具体的方法内容
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //具体的属性内容
}
三、ClassFile 的组成部分
3.1 魔数
每个Class文件头4个字节称为魔数(Magic Number),作用是用于确定这个Class文件是否能被虚拟机所接受,Class文件的魔数固定值为 0xCAFEBABE。很多文件存储标准中都使用魔数来进行身份识别(文件后缀容易被人为更改),例如图片格式(gif、png等)。
3.2 版本号
紧跟魔数的4个字节存储的是Class文件的版本号(主版本号,次版本号),即第5个字节-第8个字节。
次版本号:第5,6字节
主版本号:第7,8字节
3.3 常量池
常量池可以理解为Class的资源仓库,它是Class文件空间最大的数据项之一,长度不固定,同时它还是在Class文件中第一个出现的表类型数据项目。
- 常量池中常量的数量不固定,所以需要一个u2类型的数据来计算常量池的大小。
- 常量池的大小 = u2的大小 - 1(常量池从1开始计数),当 u2 的大小为0时,代表没有使用常量池。
所有常量池类型都具有以下通用格式:
cp_info {
u1 tag;
u1 info[];
}
3.3.1 常量池中存放的常量类型: 字面量
、符号引用
- 字面量:与Java语言层面的常量概念相近,如文本字符串、声明为final的常量值等。
- 符号引用:编译语言层面的概念,包括以下3类:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
3.3.2 常量池的项目类型
常量池中的每个项目都必须以一个 1 字节的标记开头,代表 cp_info 的类型。 info 数组的内容随 tag 的值而变化。常量池的项目类型如下图所示:
示例:
CONSTANT_Class_info 结构用于代表类或接口类型,格式如下:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
更多常量池中类型结构表可以查看链接:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4
3.3.3 通过javap查看class文件字节码内容
Class文件都是二进制格式,可通过JDK中的javap工具分析Class文件字节码。
命令:javap -v -p file_path/Test.class
,更多用法可通过javap --help查看。
3.4 (类/接口)访问标识
2个字节代表访问标志(紧随常量池之后),标志用于识别一些类或者接口层次的访问信息。
标识名 | 标识值 | 解释 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为public; |
ACC_FINAL | 0x0010 | 被声明为final;不允许子类修改 |
ACC_SUPER | 0x0020 | 当被invokespecial指令调用时,将特殊对待父类的方法 |
ACC_INTERFACE | 0x0200 | 接口标识符 |
ACC_ABSTRACT | 0x0400 | 声明为abstract;不能被实例化 |
ACC_SYNTHETIC | 0x1000 | 声明为synthetic;不存在于源代码,由编译器生成 |
ACC_ANNOTATION | 0x2000 | 声明为注释类型 |
ACC_ENUM | 0x4000 | 声明为枚举类型 |
3.5 类/父类索引
类索引和父类索引都是一个u2类型的数据,由于Java语言是单继承,故父类索引只有一个。除了java.lang.Object对象的父类索引为0,其他所有类都有父类。
3.6 接口索引
一个类可以实现多个接口,故利用interfaces_count和interfaces[interfaces_count]来描述接口信息。如果该类没有实现任何接口,则interfaces_count=0,接口的索引表不再占用任何字节。
- interfaces_count 用来记录该类所实现的接口个数。
- interfaces[interfaces_count] 用来记录所有实现的接口内容。
3.7 字段表
字段表用于描述接口或者类中声明的变量。字段包括类级变量
、实例级变量
,不包含局部变量。
每个字段由一个 field_info 结构来描述,格式如下:
field_info {
u2 access_flags; //访问标识
u2 name_index; //名称索引
u2 descriptor_index; //描述符索引
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性表的具体内容
}
字段访问标识如下:
标识名 | 标识值 | 解释 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为 public; 可以从包外部访问 |
ACC_PRIVATE | 0x0002 | 声明为 private; 只有定义的类可以访问 |
ACC_PROTECTED | 0x0004 | 声明为 protected;只有子类和相同package的类可访问 |
ACC_STATIC | 0x0008 | 声明为 static;属于类变量 |
ACC_FINAL | 0x0010 | 声明为 final; 对象构造后无法直接修改值 |
ACC_VOLATILE | 0x0040 | 声明为 volatile; 不会被缓存,直接刷新到主屏幕 |
ACC_TRANSIENT | 0x0080 | 声明为 transient; 不能被序列化 |
ACC_SYNTHETIC | 0x1000 | 声明为 synthetic; 不存在于源代码,由编译器生成 |
ACC_ENUM | 0x4000 | 声明为enum |
注意事项:
- 实际情况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED 三个标志只能出现一个。
- ACC_FINAL,ACC_VOLATILE不能同时选择。
- 接口中的字段默认包含ACC_PUBLIC、ACC_STATIC、ACC_FINAL 标识。
3.8 方法表
每个方法(构造方法和类或接口初始化的方法) 都由一个 method_info 来描述,格式如下:
method_info {
u2 access_flags; //访问标识
u2 name_index; //名称索引
u2 descriptor_index; //描述符索引
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性表的具体内容
}
方法访问标识如下:
标识名 | 标识值 | 解释 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为 public; 可以从包外部访问 |
ACC_PRIVATE | 0x0002 | 声明为 private; 只有定义的类可以访问 |
ACC_PROTECTED | 0x0004 | 声明为 protected;只有子类和相同package的类可访问 |
ACC_STATIC | 0x0008 | 声明为 static;属于类变量 |
ACC_FINAL | 0x0010 | 声明为 final; 不能被覆写 |
ACC_SYNCHRONIZED | 0x0020 | 声明为 synchronized; 同步锁包裹 |
ACC_BRIDGE | 0x0040 | 桥接方法, 由编译器生成 |
ACC_VARARGS | 0x0080 | 声明为 接收不定长参数 |
ACC_NATIVE | 0x0100 | 声明为 native; 由非Java语言来实现 |
ACC_ABSTRACT | 0x0400 | 声明为 abstract; 没有提供实现 |
ACC_STRICT | 0x0800 | 声明为 strictfp; 浮点模式是FP-strict |
ACC_SYNTHETIC | 0x1000 | 声明为 synthetic; 不存在于源代码,由编译器生成 |
- 方法的定义通过访问标志、名词索引、描述符索引表达。
- 方法中的Java代码经过编译器编译成字节码指令后,存放在方法属性表集合中的“Code”属性中。
- 如果子类没有重写(Override)父类方法,则方法集合中不会出现父类的方法信息。
- Java语言中,要重载(Overload)一个方法必须满足2个条件:方法名必须与原方法同名,特征签名不同(特征签名是指方法中各个参数在常量池的字段符号引用的集合,不包括返回值。)。
3.9 属性表
属性表可以出现在Class文件、字段表、方法表中,格式如下:
attribute_info {
u2 attribute_name_index; //属性名索引
u4 attribute_length; //属性长度
u1 info[attribute_length]; //属性的具体内容
}
属性表的限制相对宽松,不需要各个属性表有严格的顺序,只要不与已有的属性名重复,任何自定义的编译器都可以向属性表中写入自定义的属性信息,Java虚拟机运行时会忽略掉无法识别的属性。
虚拟机中预定义21项属性,下面介绍几种常见的属性,如下表所示:
属性名 | 使用位置 | 解释 |
---|---|---|
Code | 方法表 | 方法体的内容 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 声明为deprecated |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类的列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法后能描述泛型参数化类型而添加的。 |
Signature | 类、方法表、字段表 | 用于支持泛型的方法签名,由于Java的泛型采用擦除法,避免类型信息被擦除后导致签名混乱,Signature记录相关信息 |
3.9.1 Code 属性
Java程序方法体中的代码经过Javac编译器处理后,得到的字节码指令存储在Code属性内,Code属性位于方法表的属性集合中。但并非所有的方法表都必须存在这个属性,如接口或抽象类的方法就不存在Code属性。
Code_attribute 的格式如下:
Code_attribute {
u2 attribute_name_index; //常量池中的uft8类型的索引,值固定为”Code“
u4 attribute_length; //属性值长度,为整个属性表长度-6
u2 max_stack; //操作数栈的最大深度值,jvm运行时根据该值佩服栈帧
u2 max_locals; //局部变量表最大存储空间,单位是slot
u4 code_length; //字节码指令的个数
u1 code[code_length]; //具体的字节码指令
u2 exception_table_length; //异常的个数
{ u2 start_pc; //起始pc
u2 end_pc;
u2 handler_pc; //当字节码在[start_pc, end_pc)区间出现catch_type或子类,则转到handler_pc行继续处理。
u2 catch_type; //当catch_type=0,则任意异常都需转到handler_pc处理
} exception_table[exception_table_length]; //具体的异常内容
u2 attributes_count; //属性的个数
attribute_info attributes[attributes_count]; //具体的属性内容
}
slot 是虚拟机为局部变量分配内存使用的最小单位。对于byte/char/float/int/short/boolean/returnAddress等长度不超过32位的局部变量,每个占用1个Slot;对于long和double这两种64位的数据类型则需要2个Slot来存放。
3.9.2 LineNumberTable 属性
LineNumberTable 属性用于描述Java庅行号与字节码行号之前的对应关系。如果不生成LineNumberTable 属性,当程序出现异常时,堆栈中将不会显示出错的行号。
LineNumberTable_attribute 格式如下:
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc; //字节码行号
u2 line_number; //Java源码行号
} line_number_table[line_number_table_length];
}
3.9.3 LocalVariableTable 属性
LocalVariableTable 属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。如果不生成LocalVariableTable 属性,其它人引用这个方法时,参数名会消失,用类似arg0、arg1来代替;同时调试启舰无法通过参数名来获取值。
LocalVariableTable_attribute 格式如下:
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index; //
} local_variable_table[local_variable_table_length];
}
LocalVariableTypeTable 结构中用字段的特征签名(signature) 来描述。
LocalVariableTypeTable_attribute 格式如下:
LocalVariableTypeTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_type_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 signature_index; //与LocalVariableTable_attribute结构唯一的区别
u2 index;
} local_variable_type_table[local_variable_type_table_length];
}
3.9.4 ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。
实例变量:
在实例构造器方法进行赋值。
类变量:
在类构造器方法进行赋值 或 使用ConstantValue属性来赋值。
- 如果变量由final和static同时修饰(即常量),并且该变量的数据类型是基本类型或String类型,就生成ConstantValue属性来进行初始化。
- 如果变量没有被final修饰,或并非基本类型及字符串,则将会选择在方法中进行初始化。
ConstantValue_attribute 结构格式如下:
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}