JVM—Class类文件结构详解

Class类文件的结构

要了解Class文件的结构首先得了解Class文件所采用的数据类型:
Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构只有两种数据结构,即无符号数,解析Class文件全是以这两个数据结构为基础。

  • 无符号数:属于基本的数据类型,由1字节、2字节、4字节、8字节分别用u1、u2、u3、u8表示,可以用来描述数字、索引引用、数量值或者UTF-8编码构成字符串值。
  • :是由多个无符号数或者表构成的复合数据结构,习惯以"_info"结尾Class文件本质也可以看作一张表

Class文件格式

Class文件格式严格按照下表的方式进行排列构成

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count -1
u2access_flags1
u2this_class1
u2super_class1
u2interface_count1
u2interfacesinterface_count
u2fields_count1
field_infofieldsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attributes_count1
attribute_infoattributesattributes_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

1.魔数与class文件的版本

ca fe ba be //魔数值0xCAFEBABE

上述代码可以看出class文件的魔术值为0xCAFEBABE。

MagicNumber为每个Class文件的头4个字节,唯一作用就是用于判断这个文件是否为一个能被虚拟机接受的Class文件,为什么使用魔数不使用拓展名进行判断?(出于安全考虑,拓展名可以随意修改,魔术值只要没有被广泛采用就不会引起混淆)

00 00 00 34 //前两位为次版本号 后两位为主版本号

版本号为魔数后4个字节前两个字节问次版本号,后两个字节为主版本号,0034十进制表示52为java8,关于次版本号,在jdk1.2时短暂使用过,从1.2到jdk12之前次版本号均为零,未被使用过。

2.常量池

在版本号之后,则是常量池入口,常量池在class中的作用非常重要,就是class文件中的资源仓库,通常也是占用class文件空间最大的数据项目之一。
由于常量池长度不是一定的,所以在常量池的入口处需要放置一个两个字节的数据来记录常量池的容量由多大,下面例子中的字节码就表示常量池的容量为大小为18。

00 13       //00 13表示常量中常量的数量为18 

常量池的每一项都是一个表,表结构起始的第一位为一个一字节的tag标志位来表示当前常量属于哪一类常量类型,截至jdk13,常量表中分别由17中不同类型的常量,且17种常量类型各自由着完全独立的数据结构,只能逐项进行讲解。

类型标志描述
CONSTANT_Utf8_info1UTF-8编码的字符串
CONSTANT_Integer_info3整型字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整型字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类或接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12字段或方法的部分符号引用
CONSTANT_MethodType_info15表示方法句柄
CONSTANT_MethodHandle_info16表示方法类型
CONSTANT_Dynamic_info17表示一个动态计算常量
CONSTANT_InvokeDynamic_info18表示一个动态方法调用点
CONSTANT_Module_info19表示一个模块
CONSTANT_Package_info20表示一个模块中开放或者导出的包
2.1常量池中第1个常量

观察例子中常量池第一个常量,它的标志位为0x0a,查上表可得这个常量类型CONSTANT_Methoddref_info 类型,CONSTANT_Methoddref_info 类型结构为

类型名称数量描述
u1tag1值为10
u2index1指向方法的类描述符CONSTANT_Class_info索引项
u2index1指向名称及类型描述符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 项常量
2.2常量池中第2个常量

例子中常量池第二个常量,它的标志位为0x09,查上表可得常量类型CONSTANT_Fieldref_info类型结构为

类型名称数量描述
u1tag1值为9
u2index1指向方法的类描述符CONSTANT_Class_info索引项
u2index1指向名称及类型描述符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 项
2.3常量池中第3~4常量

例子中第3和第4个两个常量类型一样,它的标志位都为0x07,查上表可得常量类型**CONTANT_Class_info **类型结构为

类型名称数量描述
u1tag1值为7
u2index1指向全限定名常量的索引

结合字节码分析,该类型一共占3个字节
第一个占1个字节,用于区分索引类型
第二个占2个字节,用于指向全限定名常量项的索引

07			//表示第 3 个常量类型为 CONTANT_Class_info 长度为 3 个字节(包含标志位)
00 11       //表示全限定名存在常量池的第 17 项常量
07    		//表示第 4 个常量类型为 CONTANT_Class_info 长度为 3 个字节(包含标志位)
00 12 		//表示全限定名存在常量池的第 18 项常量
2.4 常量池中5~14及17,18常量

5到14以及17,18常量,它们标志位都是0x01,查上标可得常量类型为CONTANT_Utf8_info 类型结构为
**CONTANT_Class_info **类型结构为

类型名称数量描述
u1tag1值为1
u2length1UTF-8编码的字符串占用了字节数
u1byteslength长度为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 // 一一对应 '<init>'
<!-- 下面utf全部简略的描述 -->
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
2.5常量池15,16常量

15,16常量,它们的标志为都是0x0c查上表可得常量类型**CONSTANT_NameAndType_info **类型结构为

类型名称数量描述
u1tag1值为12
u2index1指向该字段或方法名称常量项索引
u2index1指向指向该字段或方法描述常量项索引

结合字节码分析,该类型一共占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

输出结果
!](https://img-blog.csdnimg.cn/e293555d7ddf46d08d4910b399f1529d.png)

访问标志(access_flag)

在常量池结束后,由两个2字节的访问标志,在这里标识的是类的一些基本类型以及属性如访问修饰,是否final是否abstract。
具体的访问标志位以及标志的含义有

标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类能设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,此标志位为真,其他类型为假
ACC_SYNTHETIC0x1000是否为这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举类
ACC_MODULE0x8000标识这是一个模块

查看字节码,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 接口为空

字段表集合(field_info)

字段表用于描述接口或者类中声明的变量中包含哪些信息。包括类级变量和实例级变量,但不包括在方法内部声明局部变量。
字段表的格式

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attribute_count1
attribute_infoattributesattribute_count

字段修饰符,放在access_flag中,占用2个字节,与类中的access类似,标志位和含义如下表所示

标志名称标志值含义
ACC_PUBLIC0x0001字段是否public
ACC_PRIVATE0x0002字段是否private
ACC_PROTECTED0x0004字段是否protected
ACC_STATIC0x0008字段是否static
ACC_FINAL0x0010字段是否final
ACC_VOLATELE0x0040字段是否volatile
ACC_TRANSIENT0x0080字段是否transient
ACC_SYNTHETIC0x1000字段是否由编译器自动产生
ACC_ENUM0x4000字段是否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 表示属性表集合为空

方法表集合(method_info)

方法表集合与字段表集合类似,对字段和方法的描述几乎一致,方法表的结构和字段表几乎一致,仅仅是access_flag的访问标志的类型不同。
方法表的结构如下

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attribute_count1
attribute_infoattributesattribute_count

方法表集合首先有method_count表示方法表集合中包含几个方法表
字节码如下

<!-- 方法表集合 -->
00 02 		//methods_count 表示表示方法表集合中包含 2 个方法

方法访问标志类型如下

标志名称标志值含义
ACC_PUBLIC0x0001方法是否public
ACC_PRIVATE0x0002方法是否private
ACC_PROTECTED0x0004方法是否protected
ACC_STATIC0x0008方法是否static
ACC_FINAL0x0010方法是否final
ACC_SYNCHRONIZED0x0020方法是否synchronized
ACC_BRIDGE0x0040方法是否是编译器产生的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100方法是否为native
ACC_ABSTRACT0x0400方法是否为abstract
ACC_SYNTHETIC0x1000方法是否由编译器自动产生

方法的定义通过访问索引、名称索引、描述索引进行描述,而方法内的代码通过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 个字节
<!-- attribute_info (code)-->
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 个字节
<!-- line_number_info -->
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 个字节
<!-- attribute_info (code) -->
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类型的数据 站哟 两个字节
<!-- line_number_info -->
00 00 		//start_pc 字节码行号
00 04 		//line_number java源码行号

属性表集合

属性表在之前的讲解方法表的讲解已经出现过了,不同类型属性表的结构都不同,在这里就不细说了,就介绍一下,前面出现的Code属性表和class文件最后的属性表结构。

Code属性表

Code属性表的结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2max_stack1
u2max_locals1
u4code_length1
u2codecode_length
u2exception_table_length1
exception_infoexception_tableexception_table_length
u2attributes_count1
attribute_infobattributesattribute_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属性表

LineNumberTable属性用于描述java源码行号与上传字节码行号之间的对应关系,并不是运行的必须属性,但会默认生成到class文件中,主要作用就是抛出异常是会显示行号,以及调试程序时可以根据源码行号进行设置断点。
LineNumberTable的属性结构如下

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2line_number_table_length1
line_number_infoline_number_tableline_number_table_length

line_number_table是一个l类型为line_number_info的集合,包含start_pc和line_number两个属性都是占两个字节,前者是字节码行号,后者是java源码行号。

Code属性表与LineNumberTable属性表字节码分析如下
<!-- 第一个方法 attribute_info (code)-->
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 个字节
<!-- line_number_info -->
00 00 		//start_pc 字节码行号
00 01 		//line_number java源码行号

<!--第二个方法 attribute_info (code) -->
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类型的数据 站哟 两个字节
<!-- line_number_info -->
00 00 		//start_pc 字节码行号
00 04 		//line_number java源码行号
SourceFile属性

SourceFile属性用于记录生成这个Class文件的源码文件名称,当类名和文件名不一致时抛出异常。
这个属性的结构为

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2sourcefile_index1

class文件属性表集合以及SourceFile字节码分析

<!-- 属性表 -->
00 01 		//attributes_count 表示有几个属性 占用两个字符
<!-- attribute_info -->
00 0d 		//attribute_name_index 属性名称在常量池中的索引 占用 2 个字节
00 00 00 02 //attribute_length 属性长度 占用 4 个字节
00 0e		//sourcefile_index 资源文件名称在常量池中的索引

到此为止,class文件全部解析完成,读者也可以使用该java源代码自己进行编译按照该思路进行分析,相信你们一定也会有所收获!

参考书籍:深入理解java虚拟机(第3版)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值