要了解Class文件的结构首先得了解Class文件所采用的数据类型:
Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构只有两种数据结构,即无符号数和表,解析Class文件全是以这两个数据结构为基础。
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 |
下面我们用一个实际编译的class文作为例子来分析class类文件的结构:所有的文件机构类型基于下面的class文件进行分析。
java源代码
public class Test {
private int m;
public int inc(){
return m + 1;
}
}
编译后的class文件
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 5465 7374 2e6a 6176 610c
0007 0008 0c00 0500 0601 0004 5465 7374
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7400 2100 0300 0400 0000 0100 0200
0500 0600 0000 0200 0100 0700 0800 0100
0900 0000 1d00 0100 0100 0000 052a b700
01b1 0000 0001 000a 0000 0006 0001 0000
0001 0001 000b 000c 0001 0009 0000 001f
0002 0001 0000 0007 2ab4 0002 0460 ac00
0000 0100 0a00 0000 0600 0100 0000 0400
0100 0d00 0000 0200 0e
ca fe ba be //魔数值0xCAFEBABE
上述代码可以看出class文件的魔术值为0xCAFEBABE。
MagicNumber为每个Class文件的头4个字节,唯一作用就是用于判断这个文件是否为一个能被虚拟机接受的Class文件,为什么使用魔数不使用拓展名进行判断?(出于安全考虑,拓展名可以随意修改,魔术值只要没有被广泛采用就不会引起混淆)。
00 00 00 34 //前两位为次版本号 后两位为主版本号
版本号为魔数后4个字节前两个字节问次版本号,后两个字节为主版本号,0034十进制表示52为java8,关于次版本号,在jdk1.2时短暂使用过,从1.2到jdk12之前次版本号均为零,未被使用过。
在版本号之后,则是常量池入口,常量池在class中的作用非常重要,就是class文件中的资源仓库,通常也是占用class文件空间最大的数据项目之一。
由于常量池长度不是一定的,所以在常量池的入口处需要放置一个两个字节的数据来记录常量池的容量由多大,下面例子中的字节码就表示常量池的容量为大小为18。
00 13 //00 13表示常量中常量的数量为18
常量池的每一项都是一个表,表结构起始的第一位为一个一字节的tag标志位来表示当前常量属于哪一类常量类型,截至jdk13,常量表中分别由17中不同类型的常量,且17种常量类型各自由着完全独立的数据结构,只能逐项进行讲解。
类型 | 标志 | 描述 |
---|---|---|
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 | 字段或方法的部分符号引用 |
CONSTANT_MethodType_info | 15 | 表示方法句柄 |
CONSTANT_MethodHandle_info | 16 | 表示方法类型 |
CONSTANT_Dynamic_info | 17 | 表示一个动态计算常量 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
CONSTANT_Module_info | 19 | 表示一个模块 |
CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 |
观察例子中常量池第一个常量,它的标志位为0x0a,查上表可得这个常量类型CONSTANT_Methoddref_info 类型,CONSTANT_Methoddref_info 类型结构为
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值为10 |
u2 | index | 1 | 指向方法的类描述符CONSTANT_Class_info索引项 |
u2 | index | 1 | 指向名称及类型描述符CONTANT_Name_AndType索引项 |
结合字节码分析,该类型一共占5个字节。
第一个占1个字节,用于区分索引类型,
第二个占2个字节,用于指向该类型中的CONSTANT_Class_info在常量池中的索引
第三个占2个字节,用于指向该类型中的CONSTANT_NameAndType在常量池中的索引
0a //0a表示第 1 个常量类型为CONSTANT_Methoddref_info 长度为5个字节(包含标志位)
00 04 //表示声明方法的类的字段 CONSTANT_Class_info的索引项在常量池第4项常量
00 0f //表示名称及类型字段 CONSTANT_NameAndType的索引项在常量池的第 15 项常量
例子中常量池第二个常量,它的标志位为0x09,查上表可得常量类型CONSTANT_Fieldref_info类型结构为
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值为9 |
u2 | index | 1 | 指向方法的类描述符CONSTANT_Class_info索引项 |
u2 | index | 1 | 指向名称及类型描述符CONTANT_Name_AndType索引项 |
结合字节码分析,该类型一共占5个字节。
第一个占1个字节,用于区分索引类型,
第二个占2个字节,用于指向该类型中的CONSTANT_Class_info在常量池中的索引
第三个占2个字节,用于指向该类型中的CONSTANT_NameAndType在常量池中的索引
09 //表示第 2 个常量类型为 CONSTANT_Fieldref_info 长度为5个字节(包含标志位)
00 03 //表示声明方法的类的字段 CONSTANT_Class_info 的索引项在常量池第 3 项常量
00 10 //表示字段CONSTANT_Name-AndType的索引在常量池的第 16 项
例子中第3和第4个两个常量类型一样,它的标志位都为0x07,查上表可得常量类型**CONTANT_Class_info **类型结构为
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值为7 |
u2 | index | 1 | 指向全限定名常量的索引 |
结合字节码分析,该类型一共占3个字节
第一个占1个字节,用于区分索引类型
第二个占2个字节,用于指向全限定名常量项的索引
07 //表示第 3 个常量类型为 CONTANT_Class_info 长度为 3 个字节(包含标志位)
00 11 //表示全限定名存在常量池的第 17 项常量
07 //表示第 4 个常量类型为 CONTANT_Class_info 长度为 3 个字节(包含标志位)
00 12 //表示全限定名存在常量池的第 18 项常量
5到14以及17,18常量,它们标志位都是0x01,查上标可得常量类型为CONTANT_Utf8_info 类型结构为
**CONTANT_Class_info **类型结构为
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值为1 |
u2 | length | 1 | UTF-8编码的字符串占用了字节数 |
u1 | bytes | length | 长度为length的UTF-8编码的字符串 |
结合字节码分析 | |||
第一个占1个字节,用于区分索引类型 | |||
第二个占2个字节,用于描述该常量占用多少(length)字节数 | |||
第三个占length个字节数,用于存储utf-8编码的字符串 |
01 //表示第 5 个常量类型为 CONTANT_Utf8_info 长度为 4 个字节(包含标志位)
00 01 //表示utf-8编码占用了字节数为 1
6d //6d 十进制为109 utf-8对应 m
01 //表示第 6 个常量类型为 CONTANT_Utf8_info
00 01 //表示长度为 1
49 //49 十进制为 73 utf8对应 I
01 //第 7 个常量为CONTANT_Utf8_info
00 06 //常量长度为 6
3c 69 6e 69 74 3e // 一一对应 ''
01 //第 8 个常量
00 03 //长度为 3
28 29 56 //utf对应 ()V
01 //第 9 个常量
00 04 //长度为 4
43 6f 64 65 // Code
01 //第 10 个变量
00 0f //长度为 15
4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 //LineNumberTable
01 //第 11 个变量
00 03 //长度为3
69 6e 63 //inc
01 //第 12 个变量
00 03 //长度为3
28 29 49 //()I
01 //第 13 个变量
00 0a //长度为10
53 6f 75 72 63 65 46 69 6c 65 //SourceFile
01 //第 14 个变量
00 09 //长度为9
54 65 73 74 2e 6a 61 76 61 //Test.java
······
01 //第 17 个常量为 utf-8
00 04 //长度为 4
54 65 73 74 //Test
01 //第 18 个常量为 utf-8
00 10 //长度为 16
6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 //java/lang/Object
15,16常量,它们的标志为都是0x0c查上表可得常量类型**CONSTANT_NameAndType_info **类型结构为
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值为12 |
u2 | index | 1 | 指向该字段或方法名称常量项索引 |
u2 | index | 1 | 指向指向该字段或方法描述常量项索引 |
结合字节码分析,该类型一共占5个字节
第一个占1个字节,用于区分索引类型
第二个占2个字节,指向该字段或方法名称常量项索引
第三个占2个字节,指向指向该字段或方法描述常量项索引
0c //表示第 15 个常量量类型为 CONSTANT_NameAndType_info 长度为5个字节(包含标志位)
00 07 //表示该字段的表示的字段或方法的名称为常量池的第7个常量
00 08 //表示该字段表示的字段或方法的描述为位常量池的第8个常量
0c //表示第 16 个常量量类型为 CONSTANT_NameAndType_info 长度为5个字节(包含标志位)
00 05 //表示该字段的表示的字段或方法的名称为常量池的第5个常量
00 06 //表示该字段表示的字段或方法的描述为位常量池的第6个常量
到这常量池已经全部解析完成: 可以使用javap命令输出常量表,对常量表进行验证:
javap -verbose Test
输出结果
在常量池结束后,由两个2字节的访问标志,在这里标识的是类的一些基本类型以及属性如访问修饰,是否final是否abstract。
具体的访问标志位以及标志的含义有
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类能设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志位为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 是否为这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举类 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
查看字节码,access_flag占用两个字节。
0021查上表可得该类使用invokespecial字节码指令的新语义,且被public修饰
00 21 //access_flags 标志位占用 2个字节位,0021表示被public修饰符修饰,且使用invokespecial字节码指令的新语义
类索引和父类索引都是一个两个字节的数据,而接口索引是一组两字节的数据集合,Class文件中由这三项数据来确定该类型的继承关系,类索引用于确定该类的全限类名,父类索引用于确定该类父类的全限类名。由于java语言特性不支持多类继承,所以父类索引只有一个,接口索引集合用于描述这类实现了哪些接口。
根据字节码进行分析:
00 03 //this_class 类索引,占用 2 个字节,表示在类名称存放在常量池第 3 项常量中
00 04 //super_class 父类索引,占用 2 个字节,表示父类名称存放在常量池第 4 项常量中
00 //interface_count 占用 1 个字节,表示实现接口的数量为0
00 //interfaces 接口为空
字段表用于描述接口或者类中声明的变量中包含哪些信息。包括类级变量和实例级变量,但不包括在方法内部声明局部变量。
字段表的格式
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attribute_count | 1 |
attribute_info | attributes | attribute_count |
字段修饰符,放在access_flag中,占用2个字节,与类中的access类似,标志位和含义如下表所示
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否public |
ACC_PRIVATE | 0x0002 | 字段是否private |
ACC_PROTECTED | 0x0004 | 字段是否protected |
ACC_STATIC | 0x0008 | 字段是否static |
ACC_FINAL | 0x0010 | 字段是否final |
ACC_VOLATELE | 0x0040 | 字段是否volatile |
ACC_TRANSIENT | 0x0080 | 字段是否transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否enum |
access_flag标志之后是两项索引值:name_index和descriptor_index,都是对常量池项的引用,分别表示字段简单名称和字段或方法的描述符
00 01 //fields_count 表示只有 1 个字段表数据 占用 2 个字节
00 02 //access_flags 表示字段修饰符为private 占用 2 个字节
00 05 //name_index 表示字段名称存放在常量池第 5 项常量 占用 2 个字节
00 06 //descriptor_index 表示字段方法的描述存放在常量池的第 6 项常量 占用 2 个字节
00 //attributes_count 属性表数量为0 占用 2 个字节
00 //attributes 表示属性表集合为空
方法表集合与字段表集合类似,对字段和方法的描述几乎一致,方法表的结构和字段表几乎一致,仅仅是access_flag的访问标志的类型不同。
方法表的结构如下
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attribute_count | 1 |
attribute_info | attributes | attribute_count |
方法表集合首先有method_count表示方法表集合中包含几个方法表
字节码如下
00 02 //methods_count 表示表示方法表集合中包含 2 个方法
方法访问标志类型如下
标志名称 | 标志值 | 含义 |
---|---|---|
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_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生 |
方法的定义通过访问索引、名称索引、描述索引进行描述,而方法内的代码通过javac编译存到了方法属性表中Code属性中,Code是class文最具拓展性的一部分。
下面将对两个方法表分别进行分析
00 01 //access_flags 表示方法修饰符为public 占用 2 个字节
00 07 //name_index 方法名称存放在常量池第 7 个 占用 2 个字节
00 08 //descriptor_index 表示方法描述存放在常量池第 8 个 占用 2 个字节
00 01 //attributes_count 表示此方法属性表集合中有 1 项属性 占用 2 个字节
00 09 //attribute_name_index 表示属性存放在常量池第 9 项 占用 2 个字节
00 00 00 1d //attribute_lenth 表示该属性的长度 占用 4 个字节
00 01 //max_stack 操作数栈的最大深度 占用 2 个字节
00 01 //max_locals 局部变量表所需的存储空间 占用两个字节
00 00 00 05 //code_length 表示字节码长度,占用 4 个字节
2a b7 00 01 b1 //code 用于存储编译后的字节码指令 占用code_length 个字节
00 00 //exception_table_length 异常表长度 占用 2 个字节
00 01 //code属性中的 attributes_count 占用两个字节
00 0a //attribute_name_index 属性名称在常量池的第10项常量
00 00 00 06 //LineNumber类型的 attribute_length 属性长度 占用 4 个字节
00 01 //line_number_table_length 表示有几个line_number_info类型的数据 占用 2 个字节
00 00 //start_pc 字节码行号
00 01 //line_number java源码行号
00 01 //access_flags 表示方法修饰符为public 占用 2 个字节
00 0b //name_index 方法名称存放在常量池第 11 个 占用 2 个字节
00 0c //descriptor_index 表示方法描述存放在常量池第 12 个 占用 2 个字节
00 01 //attributes_count 表示此方法属性表集合中有 1 项属性 占用 2 个字节
00 09 //attribute_name_index 表示属性存放在常量池第 9 项 占用 2 个字节
00 00 00 1f //attribute_lenth 表示该属性的长度 占用 4 个字节
00 02 //max_stack 操作数栈的最大深度 占用 2 个字节
00 01 //max_locals 局部变量表所需的存储空间 占用 2 个字节
00 00 00 07 //code_length 表示字节码的长度 占用 4 个字节
2a b4 00 02 04 60 ac// code 用于存储编译后的字节码指令 占用code_length个字节
00 00 //exception_table_length 异常表长度 占用 2 个字节
00 01 //cod属性中的 attributes_count 表示有几个属性 占用两个字节
00 0a //attribute_name_index 属性名在常量池中的索引 表示是常量池中第 10 个索引 占用 2 个字节
00 00 00 06 //LineNumber类型的 attribute_length 属性长度占用 4 个字节
00 01 //line_number_table_length 表示有几个 line_number_info类型的数据 站哟 两个字节
00 00 //start_pc 字节码行号
00 04 //line_number java源码行号
属性表在之前的讲解方法表的讲解已经出现过了,不同类型属性表的结构都不同,在这里就不细说了,就介绍一下,前面出现的Code属性表和class文件最后的属性表结构。
Code属性表的结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u2 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | battributes | attribute_count |
attribute_name_index是指向CONSTANT_Utf8_info型常量的索引,此常量固定为“Code”,代表该属性的属性名称,attribute_length表示属性长度,max_stack表示操作数栈深度的最大值,max_locals代表局部变量表所需的存储空间,max_locals单位是变量槽(虚拟机为局部变量分配内存所使用的最小单位),code_length和code是用来存储java源文件编译后生成的字节码指令,code_length代表字节码长度,code用于存储字节码指令。exception_length表示异常处理表集合长度,exception_table表示异常处理表集合,异常处理表并不是Code必须部分!
LineNumberTable属性用于描述java源码行号与上传字节码行号之间的对应关系,并不是运行的必须属性,但会默认生成到class文件中,主要作用就是抛出异常是会显示行号,以及调试程序时可以根据源码行号进行设置断点。
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是一个l类型为line_number_info的集合,包含start_pc和line_number两个属性都是占两个字节,前者是字节码行号,后者是java源码行号。
00 09 //attribute_name_index 表示属性存放在常量池第 9 项 占用 2 个字节
00 00 00 1d //attribute_lenth 表示该属性的长度 占用 4 个字节
00 01 //max_stack 操作数栈的最大深度 占用 2 个字节
00 01 //max_locals 局部变量表所需的存储空间 占用两个字节
00 00 00 05 //code_length 表示字节码长度,占用 4 个字节
2a b7 00 01 b1 //code 用于存储编译后的字节码指令 占用code_length 个字节
00 00 //exception_table_length 异常表长度 占用 2 个字节
00 01 //code属性中的 attributes_count 占用两个字节
00 0a //attribute_name_index 属性名称在常量池的第10项常量
00 00 00 06 //LineNumber类型的 attribute_length 属性长度 占用 4 个字节
00 01 //line_number_table_length 表示有几个line_number_info类型的数据 占用 2 个字节
00 00 //start_pc 字节码行号
00 01 //line_number java源码行号
00 09 //attribute_name_index 表示属性存放在常量池第 9 项 占用 2 个字节
00 00 00 1f //attribute_lenth 表示该属性的长度 占用 4 个字节
00 02 //max_stack 操作数栈的最大深度 占用 2 个字节
00 01 //max_locals 局部变量表所需的存储空间 占用 2 个字节
00 00 00 07 //code_length 表示字节码的长度 占用 4 个字节
2a b4 00 02 04 60 ac// code 用于存储编译后的字节码指令 占用code_length个字节
00 00 //exception_table_length 异常表长度 占用 2 个字节
00 01 //cod属性中的 attributes_count 表示有几个属性 占用两个字节
00 0a //attribute_name_index 属性名在常量池中的索引 表示是常量池中第 10 个索引 占用 2 个字节
00 00 00 06 //LineNumber类型的 attribute_length 属性长度占用 4 个字节
00 01 //line_number_table_length 表示有几个 line_number_info类型的数据 站哟 两个字节
00 00 //start_pc 字节码行号
00 04 //line_number java源码行号
SourceFile属性用于记录生成这个Class文件的源码文件名称,当类名和文件名不一致时抛出异常。
这个属性的结构为
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
class文件属性表集合以及SourceFile字节码分析
00 01 //attributes_count 表示有几个属性 占用两个字符
00 0d //attribute_name_index 属性名称在常量池中的索引 占用 2 个字节
00 00 00 02 //attribute_length 属性长度 占用 4 个字节
00 0e //sourcefile_index 资源文件名称在常量池中的索引
到此为止,class文件全部解析完成,读者也可以使用该java源代码自己进行编译按照该思路进行分析,相信你们一定也会有所收获!
参考书籍:深入理解java虚拟机(第3版)