Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎都是程序运行的必要数据。当遇到需要占用8位字节以上空间的数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。
Class文件格式中只有两种数据类型:无符号数和表。
整个Class文件本质上就是一张表。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,称这一系列连续的某一类型的数据位某一类型的集合。
每个Class文件的头4个字节称为魔数(Magic Number),唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件。
魔数的表示是用16进制的数:0xCAFEBABE 来表示。
在魔数后面的4个字节存储的就是Class文件的版本号:
Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1。
高版本的JDK能向下兼容以前版本的Class文件,但不能兼容以后版本的Class文件,即使文件格式并未发生变化,虚拟机也拒绝执行超过其版本号的Class文件。
常量池可以理解为Class文件之中的资源仓库,他是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
常量池中主要存放两大类常量:
常量池长度不固定
常量池的大小是不固定的,因此常量池开头放置一个u2类型的无符号数,用来存储当前常量池的容量。JVM根据这个值就知道常量池的头尾来。
这个无符号数从1开始,不是通常的从0开始
常量池中的常量由二维表来表示
常量池开头有个常量池容器计数器,接下来就全是一个个常量了,只不过常量都是由一张张二维表构成,除了记录常量的值以外,还记录当前常量的相关信息。
Class文件之中的资源仓库
Class文件结构中与其他项目关联最多的数据类型
占用Class文件空间最大的数据项目之一
刚才介绍了,常量池中的常量大体上分为:字面值常量 和 符号引用。在此基础上,根据常量的数据类型不同,又可以被细分为14种常量类型。这14种常量类型都有各自的二维表示结构。每种常量类型的头1个字节都是tag,用于表示当前常量属于14种类型中的哪一个。
以CONSTANT_Class_info常量为例,它的二维表示结构如下:
CONSTANT_Class_info表:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
tag表示当前常量的类型(当前常量为CONSTANT_Class_info,因此tag的值应为7,表示一个类或接口的全限定名);
name_index表示这个类或接口全限定名的位置。它的值表示指向常量池的第几个常量。它会指向一个CONSTANT_Utf8_info类型的常量,它的二维表结构如下:
CONSTANT_Utf8_info表:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
CONSTANT_Utf8_info表示字符串常量;
tag表示当前常量的类型,这里应该是1;
length表示这个字符串的长度;
bytes为这个字符串的内容(采用缩略的UTF8编码)
访问标志(access_flags)占用两个字节,这个标志用于识别一些类或者接口层次的访问信息:
访问标志中一共有16个标志位可用,当前只用了8个,其他要求一律为0。
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,接口索引集合(interfaces)是一组u2类型的数据的集合。
Class文件由这三项数据来确定这个类的继承关系:
类索引确定这个类的全限定名
父类索引用于确定这个类的父类的全限定名
除java.lang.Object
外,所有Java类的父类索引都不为0。
接口索引集合描述这个类实现了哪些接口
被实现的接口按接口顺序从左到右排列在接口索引集合中。
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的所引致指向一个类型为CONSTANT_Class_info的类描述符常量,该常量的bytes字段记录了本类、父类的全限定名。
由于一个类的接口可能有好多个,因此需要用一个集合来表示接口索引,它在类索引和父类索引之后。这个集合头两个字节表示接口索引集合的长度,接下来就是接口的名字索引。
字段表用于描述接口或者类中生命的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
access_flags
字段的访问标志。
name_index
本字段名称的索引,指向一个CONSTANT_Class_info类型的常量,存储了本字段的名字等信息。
descriptor_index
字段和方法的描述符,用于描述本字段在Java中的数据类型等信息。
attributes_count
属性表集合的长度。
attributes
属性表集合。
全限定名就是把类全名中的.
替换成了/
而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个;
来表示全限定名结束。
简单名称就是指没有类型和参数修饰的方法或者字段名称。
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
在描述符中:
标识字符 | 含义 | 标识字符 | 含义 |
---|---|---|---|
B | 基本类型byte | J | 基本类型long |
C | 基本类型char | S | 基本类型short |
D | 基本类型double | Z | 基本类型boolean |
F | 基本类型float | V | 特殊类型void |
I | 基本类型int | L | 对象类型,如Ljava/lang/Object |
对于数组类型,每一唯独将使用一个前置的“[”字符来描述,如一个定义为java.lang.String[][]
类型的二维数组,将被记录为:[[Ljava/lang/String]]
,一个整形数组int[]
将被记录为:[I
。
对于描述方法,按照“先参数列表,后返回值的顺序描述”,参数列表按照参数的严格顺序放在一组小括号“()”内。例如:void inc()
的描述符为:()V
。方法java.lang.String.toString()
的描述符为:()Ljava/lang/String
。
方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)
的描述符为:([CII[CIII)I
。
在class文件中,所有的方法以二维表的形式存储,每张表来表示一个函数,一个类中的所有方法构成方法表的集合。
方法表的结构和字段表的结构一致,只不过访问标志和属性表集合的可选项有所不同。
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法中的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具拓展性的一种数据项目,在第8节中讲。
在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
属性表集合对于各个属性表的顺序不再做严格要求,只要不与已有属性名重复即可。任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
为了正确解析Class文件,虚拟机与定义了一些虚拟机实现应当能识别的属性,对于这些属性,它的名称需要从常量池中引用一个CONSTANT_Utf9_info类型的常量来表示,而属性值的结构则是完全自定义的。
一个符合规则的属性表应该有如下结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |