1、Class文件是一组以8位字节为基础单元的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何的分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割为若干个8位字节进行存储。
2、按照Java虚拟机规定,Class文件格式采用一种类似C语言结构体的伪机构来存储,这种伪结构中只有两种数据类型:无符号数和表。
3、无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值、或者按照UTF-8编码构成字符串值。
4、表是由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由如下所示的数据项构成。
类型 | 名称 | 数量 |
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interface_count | 1 |
u2 | interfaces | interface_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
5、cp_info,常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,人文本字符串、被声明为
final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
常量池中的每一项常量都是一个表,共有11种结构各不相同的表结构数据,这11种表都有一个共同的特点,就是表开始的第一位是一个url类型的标志符(tag,取值为1至12,缺少标志位2的数据类型),代表当前这个常量属于哪种常量类型,11种常量类型所代表的具体含义如下:
类型 | 标志 | 描述 |
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
之所以说常量池是最繁琐的数据,是因为这11种常量类型各自均有自己的结构。11种常量池的结构定义总结如下:
常量 | 项目 | 类型 | 描述 |
CONSTANT_Utf8_info | tag | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用了字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 | |
CONSTANT_Integer_info | tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的int值 | |
CONSTANT_Float_info | tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的float值 | |
CONSTANT_Long_info | tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的long值 | |
CONSTANT_Double_info | tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的double值 | |
CONSTANT_Class_info | tag | u1 | 值为7 |
index | u2 | 指定全限定名常量项的索引 | |
CONSTANT_String_info | tag | u1 | 值为8 |
index | u2 | 指定字符串字面量的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值为9 |
index | u2 | 指定声明字段的类或接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_Methodref_info | tag | u1 | 值为10 |
index | u2 | 指定声明方法的类描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指定名称及类型描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_InterfaceMethodref_info | tag | u1 | 值为11 |
index | u2 | 指向声明方法的接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指定名称及类型描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_NameAndType_info | tag | u1 | 值为12 |
index | u2 | 指向该字段或方法名称常量项的索引 | |
index | u2 | 指向该字段或方法描述符常量项的索引 |
可以使用javap工具的-verbose参数输出TestClass.class文件的字节码内容。
6、字段表(field_info)用于描述接口或类中声明的变量。字段(field)包括了类级变量或实例级变量,但不包括在方法内部声明的变量。字段表结构如下:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
7、方法表的结构同字段表一样,仅在访问标志和属性表集合的可选性中有所区别。
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
8、属性表(attribute_info)在Class文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景专有的信息。
与Class文件中其他数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,java虚拟机运行时会忽略掉它不认识的属性。为了能正确地解析Class文件,《Java虚拟机规范(第二版)》中预定义了9中虚拟机应当能识别的属性,具体如下所示:
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件描述 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
Code:Java程序方法体里面的代码经过javac编译器处理之后,最终变为字节码指令存储在Code属性表内。Code属性出现在方法表的属性集合之中,但并非所有的方法都必须存在这个属性,譬如接口或抽象方法就不存在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 |
LineNumberTable:LineNumberTable属性用于描述Java源码行号与字节行号(字节码的便宜量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在javac中使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行 产生的最主要的影响就是在抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候无法按照源码来设置断点。LineNumberTable属性的结构如下表所示:
类型 | 名称 | 数量 |
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_table是一个数量为line_number_table_length,类型为line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。
类型 | 名称 | 数量 |
u2 | start_pc | 1 |
u2 | line_number | 1 |
LocalVariableTable:LocalVariableTable属性用于描述栈帧中局部表量表中的变量与Java源码中定义的变量之间的关系,它不是运行时必需的属性,默认页不会生成到Class文件之中,可以再Javac中使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响是当其他人引用这个方法时,所有的参数名称都将丢失,IDE可能会使用诸如arg0、arg1之类的占位符来代替原有的参数名,这对程序运行没有影响,但是会给代码编写带来较大的不便,而且在调试期间调试器无法根据参数名称从运行上下文中获得参数值。LocalVariableTable属性的结构如下表所示:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | atribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
其中local_variable_info项目代表了一个栈帧与源码中的局部变量的关联,start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用范围。
name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称及局部变量的描述符。结构如下表所示。
类型 | 名称 | 数量 |
u2 | start_pc | 1 |
u2 | length | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | index | 1 |
index是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量的数据类型时64位类型时(double和long),它占用的Slot为index和index+1两个位置。