Class文件是一组以8bit为基础的二进制流,各个数据项严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据时,按照高位在前的方式进行存储。
根据JVM规范规定,Class文件采用一种类似于C语言结构体的形式来存储,这种伪结构中只有两种数据类型:无符号数和表。
无符合号数属于基本数据类型,以u1、u2、u3、u4分别代表1个字节、2个字节、3个字节和4个字节的无符号数。无符号数可以用于描述数字、索引引用、数量值或按照UTF-8编码构成字符串值。
表是由多个无符号数或其他表为数据项构成的复合数据类型。习惯上,所有表都以_info续表。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
表格1 Class文件格式
类型 | 名称 | 数量 |
u4 | magic number | 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 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
acctribute_info | attributes | attributes_count |
我们以一个简单的例子来说明一下Class文件的格式,源代码为:
interface A
{
public void sayA();
}
interface B
{
public void sayB();
}
public class ClassFileDemo implements A, B
{
public static void main(String[] args)
{
ClassFileDemo demo = new ClassFileDemo();
demo.getNumberA();
demo.getStringB();
}
public int getNumberA()
{
return a;
}
public String getStringB()
{
return b;
}
public void sayA()
{
}
public void sayB()
{
}
private int a = 0;
private String b = “”;
}
编译后的class文件如下图所示:
文件的头四个字节为magic number,固定为CA FE BA BE,其主要作用为确定该文件是否为一个能被虚拟机接受的Class文件。从第五到第八个字节,共四个字节,存储的是Class文件的版本号;前字节为次版本号,后两个字节为主版本号。Java版本号从45开始,从JDK1.1之后每发布一个大版本,主版本号就向上加1(JDK1.0-1.1使用了45.0-45.3的版本号)。高版本的JDK能够向下兼容以前版本的Class文件,但不能运行之后版本的Class文件。
从第九个字节开始,就进入到class文件的常量池了。第九、十两个字节,表示常量池条目的数量,00 28换算成十进制就是40,代表常量池中有39个条目[1]。常量池中主要存储两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量概念比较接近接近Java语言层面的常量概念,如字符串、被声明为final的常量值等。符号引用主要包含类、接口的全限定名、字段名称和描述符、方法名称和描述符三大类内容。
常量池的每一项常量都是一个表,共有11种不同的表结构。这些表结构的第一位是一个u1类型的标识位,取值为1-2(缺少2的数据类型),标识当前常量属于哪种类型,其具体含义如下表所示:
表格 常量池的项目类型
类型 | 标志 | 描述 |
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 | 字段或方法的部分符号引用 |
回到具体的例子上来,偏移量为0000000A地址上,我们可以看到值为0A,十进制数为10,即第一项为CONSTANT_Methodref_info类型。我们从“常量池中的11种数据类型的结构总表”中得知,该类型共包含5个字节,0A 00 09 00 1E,0009指向常量池的第9项,001E指向常量池的第30项。常量池第一项解析完毕。
第二项:标志位为09,该类型为CONSTANT_Fieldref_info,共占有5个字节,09 00 05 00 1F,00 09指向常量池的第5项,00 1F指向常量池的第31项。
第三项:标志位为08,该类型为CONSTANT_String_info,后面紧跟的两个字节00 20,指向常量池中的第32项。
第四项:标志位为09,类型同第二项,共占用5个字节,09 00 05 00 21,00 05指向常量池中的第五项,00 21指向常量池中的第33项。
第五项:标志位为07,类型为CONSTANT_Class_info,占用3个字节,00 22指向常量池中的第34项。
第六项:标志位为0A,类型同常量池第一项,占用5个字节 0A 00 05 00 1E,其中00 05指向常量池中第5项,00 1E指向常量池中第30项。
第七项:标志位为0A,类型同第六项,占用5个字节0A 00 05 00 23,其中00 05指向常量池中第5项,00 23指向常量池中第35项。
第八项:标志位为0A,类型同第七项,占用5个字节0A 00 05 00 24,其中00 05指向常量池中第5项,00 24指向常量池中第36项。
第九项:标志位为07,类型为CONSTANT_Class_info,占用3个字节07 00 25,其中00 25指向常量池中第37项。
第十项:标志位为07,类型为CONSTANT_Class_info,占用3个字节07 00 26,其中00 26指向常量池中第38项。
第十一项:标志位为07,类型为CONSTANT_Class_info,占用3个字节07 00 27,其中00 27指向常量池中第39项。
第十二项:标志位为01,类型为CONSTANT_Utf8_info,占用4个字节 01 00 01 61,其中长度为00 01,值为61,显示为a。
第十三项:标志位为01,类型为CONSTANT_Utf8_info,占用4个字节 01 00 01 49,其中长度为00 01,值为49,显示为I。
第十四项:标志位为01,类型为CONSTANT_Utf8_info,占用4个字节 01 00 01 62,其中长度为00 01,值为62,显示为b。
第十五项:标志位为01,类型为CONSTANT_Utf8_info,占用21个字节,01 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b,其中长度为00 12,字符串显示为Ljava/lang/String;
第十六项:标志位为01,类型为CONSTANT_Utf8_info,占用9个字节,01 00 06 3c 69 6e 69 74 3e,其中长度为00 06,字符串显示为<init>
第十七项:标志位为01,类型同第十六项,占用6个字节,01 00 03 28 29 56,其中长度为00 03,字符串显示为()V
第十八项:标志位为01,类型同第十六项,占用7个字节,01 00 04 43 6f 64 65,其中为00 04,字符串显示为Code
第十九项:标志位为01,类型同第十六项,占用18个字节,01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65,其中长度为00 0f,字符串显示为LineNumberTable
第二十项:标志位为01,类型同第十六项,占用7个字节,01 00 04 6d 61 69 6e,其中长度为00 04,字符串显示为main
第二十一项:标志位为01,类型同第十六项,占用25个字节,01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56,其中长度为00 16,字符串显示为([Ljava/lang/String;)V
第二十二项:标志位为01,类型同第十六项,占用13个字节,01 00 0a 67 65 74 4e 75 6d 62 65 72 41,长度为00 0a,字符串显示为getNumberA。
第二十三项:标志位为01,类型同第十六项,占用6个字节,01 00 03 28 29 49,长度为00 03,字符串显示为()I
第二十四项:标志位为01,类型同第十六项,占用13个字节,01 00 0a 67 65 74 53 74 72 69 6e 67 42,长度为00 0a,字符串显示为getStringB
第二十五项:标志位为01,类型同第十六项,占用23个字节,01 00 14 28 29 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b,长度为00 14,字符串显示为()Ljava/lang/String;
第二十六项:标志位为01,类型同第十六项,占用7个字节,01 00 04 73 61 79 41,长度为00 04,字符串显示为sayA
第二十七项:标志位为01,类型同第十六项,占用7个字节,01 00 04 73 61 79 42,长度为00 04,字符串显示为sayB
第二十八项:标志位为01,类型同第十六项,占用13个字节,01 00 0a 53 6f 75 72 63 65 46 69 6c 65,长度为00 0a,字符串显示为SourceFile
第二十九项:标志位为01,类型同第十六项,占用21个字节,01 00 12 43 6c 61 73 73 46 69 6c 65 44 65 6d 6f 2e 6a 61 76 61,长度为00 12,字符串显示为ClassFileDemo.java
第三十项:标志位为0c,类型为CONSTANT_NameAndType_info,占用5个字节,0c 00 10 00 11,其中00 10指向常量池中第16项,00 11指向常量池中第17项。
第三十一项:标志位为0c,类型同第三十项,占用5个字节,0c 00 0c 00 0d,00 0c指向常量池中第12项,00 0d指向常量池中第13项。
第三十二项:标志位为01,类型同第十六项,占用3个字节,01 00 00
第三十三项:标志位为0c,类型同第三十项,占用5个字节,0c 00 0e 00 0f,00 0e指向常量池中第14项,00 0f指向常量池中第15项。
第三十四项:标志位为01,类型同第十六项,占用16个字节,01 00 0d 43 6c 61 73 73 46 69 6c 65 44 65 6d 6f 0c,长度为00 0d,字符串显示为ClassFileDemo。
第三十五项:标志位为0c,类型同第三十项,占用5个字节,0c 00 16 00 17,00 16指向常量池中第22项,00 17指向常量池中第23项。
第三十六项:标志位为0c,类型同第三十项,0c 00 18 00 19,0018指向常量池中24项,0019指向常量池中第25项。
第三十七项:标志位为01,类型同第十六型,占用19个字节,01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74,长度为0010,字符串显示为java/lang/Object
第三十八项:标志位为01,类型同第十六项,占用4个字节,01 00 01 41,长度为0001,字符串显示为A
第三十九项:标志位为01,类型同第十六项,占用个字节,01 00 01 42,长度为0001,字符串显示为B
常量池中的11种数据类型的结构总表
常量 | 项目 | 类型 | 描述 |
CONSTANT_Utf8_info | tag | u1 | 1 |
length | u2 | ||
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 | 指向该字段或方法描述符常量项的索引 |
常量池解析完毕之后的两个字节,代表access_flags,值为00 21。access_flags用于标识一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。具体信息见下表所示:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令,JDK1.2之后编译出来的类这个标志为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或抽象类来说,此标志值为真,其它类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
本类符合ACC_PUBLIC和ACC_SUPER这两个标志,所以结果为0021。
接下来的两个字节描述this_class,0005,指向常量池中的第五项。
接着的两个字节描述super_class,0009,指向常量池中的第九项。
接着的两个字节描述实现接口的个数interfaces_count,0002,表示实现了两个接口。紧接着用四个字节来指示接口的索引,前两个字节000a指向常量池中第十项,后两个字节000b指向常量池中的第十一项。
接着开始解析字段,0002表示本类中有两个字段,字段表的描述如下表:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段的access_flags同类的access_flags含义非常相似,如下表
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 字段是否为public类型 |
ACC_PRIVATE | 0x0002 | 字段是否为private类型 |
ACC_PROTECTED | 0x0004 | 字段是否为protected类型 |
ACC_STATIC | 0x0008 | 字段是否static |
ACC_FINAL | 0x0010 | 字段是否final |
ACC_VOLATILE | 0x0040 | 字段是否volatile |
ACC_TRANSIENT | 0x0080 | 字段是否transient |
ACC_SYNTHETIC | 0x0100 | 字段是否是由编译器自动产生的 |
ACC_ENUM | 0x4000 | 字段是否enum |
本例中access_flags为0002,表示此字段类型private,name_index为000c,表示引用常量池中的第十二项,name值为a,descriptor_index值为000d,表示引用常量池中的第十三项,其值为I。
描述符的作用是用于描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值,根据描述符规则,基本类型类型及代表无返回值的void类型都用一个大写字母来进行表示,而对象类型则用字符L加对象的全限定名来表示,详见下表
标识字符 | 含义 |
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 对象类型 |
对于数组类型,每一维度用一个前置的[来进行描述。
根据上述描述可得知该定做为整型数值。
接着两个节目0000表示该字段的attributes_count为0。即可以下一个字段的解析,0002为access_flags,000e为name_index,000f为descriptor_index。0000代表attributes长度为0。
至此为止,字段信息解析完毕,接下来开始解析方法信息。
0006表示有6个方法表,方法表结构描述信息如下:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法表的访问标识定义如下表:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 方法是否为public类型 |
ACC_PRIVATE | 0x0002 | 方法是否为private类型 |
ACC_PROTECTED | 0x0004 | 方法是否为protected类型 |
ACC_STATIC | 0x0008 | 方法是否static |
ACC_FINAL | 0x0010 | 方法是否final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否synchronized |
ACC_BRIDGE | 0x0040 | 方法是否由编译器产生的桥接方法 |
ACC_VARAGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否native |
ACC_ABSTRACT | 0x0400 | 方法是否abstract |
ACC_STRICT | 0x0800 | 方法是否strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生的 |
第一个方法表,access_flags为0001,表明该方法为public的,name_index为0010,引用常量池中第16项,名字为<init>,descriptor_index为0011,引用常量池中的第17项()V,表示无返回值,attributes_count为0001,表示有1条属性,我们接着解析属性表,虚拟机规范预定义的属性如下表所示:
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类、方法表、字段表 | 标识方法或字段是由编译器自动生成的 |
0012引用常量池中的第18项,为Code,表示该属性是Code属性。我们接着来看一下Code属性的具体定义:
Code属性表的结构
类型 | 名称 | 数量 |
u2 | attributes_name_index | 1 |
u4 | attributes_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_lengh |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
00000030,表示attributes_length为48,max_stack为0002,max_locals为0001,code_length为00000010,16个字节,2a b7 00 01 2a 03 b5 00 02 2a 12 03 b5 00 04 b1,每个字节表示一条字节码指令,0000表示异常表长度为0,0001表示属性长度为1,继续解析属性表,0013引用第19项常量,其值为LineNumberTable,我们再来看一下LineNumberTable属性的定义情况:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_table_info | line_number_table_info | line_number_table_length |
000000e,该属性长度为14,0003表示有3个line_number_table,line_number_table包含两个u2类型的数据,前一个表示字节码行号,后一个表示源码行号。
[1] 由于常量池索引计算是从1开始的,而不是其他程序意义上的从0开始,所以为39个条目。制定Class文件格式规范时,将第0项空出来是有特殊考虑的,这样做是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达为“不引用任何一个常量池项目”的意思,这样的情况就可以把索引位置置为0来表示。