类文件结构

Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class”文件这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。

可以说Class文件是不同的语言在Java虚拟机之间的重要桥梁,同时也是支持Java跨平台很重要的一个原因。

Class类文件结构总览

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格地按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位子节进行存储。

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。

Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所以表都习惯性地以”_info”结尾,表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。

Class文件格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
u4             	magic; 	//Class 文件的标志
u2 minor_version; //Class 的小版本号
u2 major_version; //Class 的大版本号
u2 constant_pool_count; //常量池的数量
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; //Class 的访问标志
u2 this_class; //当前类
u2 super_class; //父类
u2 interfaces_count; //接口
u2 interfaces[interfaces_count]; //一个类可以实现多个接口
u2 fields_count; //Class 文件的字段属性
field_info fields[fields_count]; //一个类会可以有个字段
u2 methods_count; //Class 文件的方法数量
method_info methods[methods_count]; //一个类可以有个多个方法
u2 attributes_count; //此类的属性表中的属性数
attribute_info attributes[attributes_count]; //属性表集合

无论是无符号数还是表,当需要描述同一类型数据但数量不定时,经常会使用一个潜质的容量计数器加若干个连续的数据项形式,这时称这一系列的某一类型的数据为某一类型的集合。

Class文件字节码结构组织示意图:

魔数

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用就是确定这个文件是否为一个能被虚拟机接受到Class文件,很多文件存储标准中都使用魔数来进行身份识别而不是扩展名的主要原因是基于安全方面的考虑,因为文件扩展名可以随意的改动。

1
u4             magic;	//Class 文件的标志

Java中Class文件的魔数值为:0xCAFEBABE(咖啡宝贝?)。

Class文件版本

紧接着魔术的4个字节存储的是Class文件的版本号:第5个和第6个是次版本号(Minor Version),第7个和第8个是主版本号(Major Version)

1
2
u2             minor_version;	//Class 的次版本号
u2 major_version; //Class 的主版本号

Java版本号是从45开始的,JDK1.1之后每个JDK大版本发布主版本号向上加一,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化。

常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是战役Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。

1
2
u2             constant_pool_count;	//常量池的数量
cp_info constant_pool[constant_pool_count-1]; //常量池

常量池的入口为一项u2类型的数据,代表常量池容量计数值(constant_pool_count),这个容量计数是从1开始而不是从0开始的,值为0代表“不引用任何一个常量池项目”的特殊情况。Class文件结构中只有常量池的容量计数是从1开始的。

常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final类型的常量值等;而符号引用则属于编译原理方面的概念,包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

在Class文件之不会保存各个方法、字段的最终内存的布局信息,当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池中每一个项都是一个表,这14种表有一个共同的特点:表开始的第一位是一个u1类型的标志位(tag),代表这个常量属于哪种常量类型。

由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535,所以Java程序中如果定义可超过64KB英文字符的变量或方法名,将会无法编译。

Class 文件可以通过javap -v class类名 指令来看一下其常量池中的信息(javap -v class类名-> temp.txt :将结果输出到 temp.txt 文件)。

常量池中一部分自动生成的常量的确都没有在Java代码里面出现过,但它们会被后面即将讲到的字段表(field_info)、方法表(method_info)、属性表(attribute_info)引用到,他们会用来描述一些不方便使用“固定字节”进行表达的内容,譬如描述方法的返回值是什么?有几个参数?每个参数的类型是什么?

常量池中的14种常量项的结构总表:

访问标志

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是接口还是类;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否声明为final等。

1
u2		access_flags;	//Class 的访问标记


最后这两个字节的值是上面这8个标志位的值的异或结果,没有用到的标志位一律为0。

类索引、父类索引与接口索引集合

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项来确定这个类的继承关系。

1
2
3
4
u2             this_class;	//当前类
u2 super_class; //父类
u2 interfaces_count; //接口
u2 interfaces[interfaces_count]; //一个类可以实现多个接口

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。除了Java.lang.Object之外,所有的Java类都有父类,因此除了Java.lang.Object之外,所有Java类的父类索引都不为0,

接口索引集合就用来描述这个类实现了哪些接口,这些实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后;对于接口索引集合,入口的第一项————u2类型的数据为接口计数器,表示索引表的容量,如果该类没有实现任何接口,则该计数器值为0。

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

1
2
u2             fields_count;	//Class 文件的字段的个数
field_info fields[fields_count]; //一个类会可以有个字段

字段表的结构:

字段修饰符放在access_flags项目中,在实际情况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择,接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,其可以设置的标志位和含义如图:

跟随access_flags标志的是两项索引值:name和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。

全限定名:一个类的全名的“.”全部替换成“/”
简单名称:没有类型和参数修饰的方法或字段名称,既只有名字
描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型和顺序)和返回值。描述符标识字符含义:对于数组来说,每一维度将使用一个前置的“[”字符来描述;描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”内

descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在描述表中描述零至多项的额外信息。

字段表集合中不会列出从超累或者父类中继承来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外如果两个字段的描述符不一致,那字段重名就是合法的。

方法表集合

方法表的结构如同字段表一样:

1
2
u2             methods_count;	//Class 文件的方法的数量
method_info methods[methods_count]; //一个类可以有个多个方法


方法表的 access_flag 取值:

方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目,见下一节。

与字段表集合相对应的,如果父类方法在子类中没有进行重写(Override),方法表集合中就不会出现来自弗雷德方法信息。但同样有可能出现由编译器自动添加的方法,最典型的便是类构造器方法和实例构造器方法。

Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言无法仅仅依靠返回值的不同来对一个已有方法进行重载。

属性表集合

在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

1
2
u2             attributes_count;	//此类的属性表中的属性数
attribute_info attributes[attributes_count]; //属性表集合

属性表的限制相对其他的数据项目稍微宽松了一些,不再要求各个属性表具有严格的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略它不认识的属性。

对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来标识,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。

Code属性

Java程序方法体中的代码经过Javac编译器处理后,最后变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。

attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表该属性的属性名称。

attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节。

max_stack代表了操作栈数深度的最大值。

max_locals代表了局部变量表所需的存储空间,单位是Slot,Slot时虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAdress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这种64位的数据类型则需要两个Slot来存放。并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超过一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。

code_length和code用来存储Java源程序编译后生成的字节码指令,code_length代表字节码长度,code是用于存储字节码指令的一系列字节流,每个指令长度为u1类型的单字节,虚拟机每次读入一个字节码指令。关于code_length,虽然他是一个u4类型的长度值,但虚拟机明确限制了一个方法不允许超过65535条字节码指令,即它实际只使用了u2的长度。

在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出一个Slot位来存放对象实例的引用,方法参数值从1开始计算。

exception_table:异常表如下。如果当字节码在第start_pc行到第end_pc行之间(不含end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的引用),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转向到handler_pc行进行处理。

Exceptions属性

Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Exceptions),也就是方法描述时在throws关键字后面列举的异常。

Exceptions属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常。每一种受查异常使用一个number_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。

LineNumberTable属性

LineNumberTable属性用于描述Java源码行号与字节码(字节码的偏移量)之间的对应关系。

line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。

LocalVariableTable属性

LocalVariableTable属性用于描述栈帧中局部变量表中与Java源码中定义的变量之间的关系。

start_pc和length属性分别代表了这个局部变量的生命周期开始地字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。

name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。

index是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),他占用的Slot为index和index+1两个。

在JDK1.5引入泛型之后,LocalVariableTable属性增加了一个“姐妹属性”:LocalVariableTypeTable,这个新增的属性结构与LocalVariableTable非常相似,仅仅是吧记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature),对于非泛型类型来说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入后,由于描述符中反省的参数化类型被擦除掉,描述符就不能准确的描述泛型类型了,因此出现了LocalVariableTypeTable。

SourceFile属性

SourceFile属性用于记录生成这个Class文件的源码文件名称。

sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名。

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。对于非static类型的变量的赋值是在实力构造器方法中进行的;而对于类变量,则有两种方式可以选择:再类构造器方法中或者使用ConstantValue属性。

从数据结构中可以看出,ConstantValue属性是一个定长属性,他的attribute_length数据项值必须固定为2。constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种。

InnerClasses属性

Inner属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。

Deprecated及Synthetic属性

Depreciated和Syntactic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。

Depreciated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated注解进行设置。

Syntactic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。唯一例外的是实例构造器方法和类构造器方法。

StackMapTable属性

StackMapTable属性在JDK1.6发布后增加到Class文件规范中,它是一个复杂的变长属性,位于Code属性的属性表中。

这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

Signature属性

Signature属性在KJDk1.5发布后增加到了Class文件规范中,他是一个可选的定长类属性,可以出现于类、字段表、和方法表结构的属性表中。

在JDK 1.5大幅增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。之所以要专门使用这样一个属性去记录泛型类型,是因为Java语言的泛型采用的是擦除法实现的伪泛型。

使用擦除法的好处是实现简单(主要修改Javac编译器,虚拟机内部只做了很少的改动)、非常容易实现Backport,运行期也能够节省一些类型所占的内存空间。但坏处是运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得到泛型信息。Signature属性就是为了弥补这个缺陷而增设的。

BootstrapMethods属性

BootstrapMethods属性在JDK1.7发布后增加到了Class文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokeddynamic指令引用的引导方法限定符。目前的Javac暂时无法生成InvokeDynamic指令和BootstrapMethods属性,必须通过一些非常规的手段才能使用它们。