本博文目的:通过本片博文的阅读,希望大家能够能class文件格式有一个直观的认识,同时对其中的一些实现细节也能有一定的掌握。本文并不会说的太细,太细节化的东西,真的太多,涉及到一大堆属性和验证的过程。我都看烦了,不仅记不住,而且也不是我们所关心的东西。
每一个class文件都对应着唯一一个类或接口的定义信息,每个class文件都由字节流组成,多字节的数据项总是按照big-endian(大端在前)的顺序进行存储。接下来将采用类似C语言结构体的伪结构来描述class文件格式。
ClassFile结构
- magic(魔数)
magic的唯一作用是确定这个文件是否为一个能被虚拟机所接受的class文件。魔数数值固定为0xCAFEBABE(看到这个数值,是否想到了java的图标,咖啡。。),不会改变。 - minor_version(副版本号)、major_version(主版本号)
若某个class文件的主版本号为M,副版本号为m,那么这个class文件格式版本号确定为M.m。 - constant_pool_count(常量池计数器)
此值等于常量池表中的成员数加1。 - constant_pool[] (常量池)
constant_pool是一种表结构,它包含class文件结构及子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池以1~constant_pool_count - 1为索引。 - access_flags
access_flags是一种由标志所构成的掩码,用于表示某个类或者接口的访问权限及属性。
标志名 | 值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为public,可以从包外访问 |
ACC_FINAL | 0x0010 | 声明为final,不允许有子类 |
ACC_SUPER | 0x0020 | 当用到invokespecial指令时,需要对父类方法做特殊处理 |
ACC_INTERFACE | 0x0200 | 该class文件定义的是接口而不是类 |
ACC_ABSTRACT | 0x0400 | 声明为abstract,不能被实例化 |
ACC_SYNTHETIC | 0x10000 | 声明为synthetic,表示该class文件并不是由Java源代码生成 |
ACC_ANNOTATION | 0x2000 | 标识注解类型 |
ACC_ENUM | 0x4000 | 标识枚举类型 |
6. this_class(类索引)
this_class的值必须是对常量池中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Class_info类型的结构体。
7. super_class(父类索引)
对于类来说,super_class的值要么是0,要么是对常量池表中某项的一个有效索引值。如果它不是0,那么常量池在这个索引处的成员必须为CONSTANT_Class_info类型常量。
8. interfaces_count(接口计数器)
interfaces_count表示当前类或接口的直接超接口的数量。
9. interfaces[] (接口表)
interfaces[]中的每个值都是常量池表中的某一项的有效索引值,它的长度为interfaces_count。每个成员interfaces[i]必须为CONSTANT_Class_info结构。
10. fields_count(字段计数器)
表示当前class文件fields表的成员个数。
11. fields[] (字段表)
fields表中的每个成员都必须是一个fields_info结构。
12. methods_count(方法计数器)
表示当前class文件methods表中的成员个数
13. methods[] (方法表)
methods表中的每个成员都必须是一个method_info结构。
14. attributes_count(属性计数器)
表示当前class文件属性表的成员个数。
15. attributes[] (属性表)
属性表的每个项的值都是attribute_info结构。
常量池
Java虚拟机指令不依赖于类、接口、类实例或数组的运行时布局,而是依赖常量池表中的符号信息。可见常量池是多么的重要。。
常量池表中的所有项都具有如下通用格式:
cp_info {
u1 tag;
u2 info[];
}
每个cp_info项都必须以一个表示cp_info类型的单字节“tag”项开头。后面info[]数组的内容由tag的值所决定。tag可取值如下所示:
常量类型 | 值 |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
针对于上面的每一个值,都会有一种数据结构与之对应。比如当tag项的值为CONSTANT_Class时,对应的数据结构为:
CONSTANT_Class_info {
u1 tag;
u2 name_index;//此值是对常量池表的一个有效索引。且该索引处的成员为CONSTANT_Utf8_info结构。
}
对于tag的其他值,大家可以查看相关的文章。。这里就不一一赘述了。。
字段
每个字段都由field_info结构所定义。field_info结构如下:
field_info {
u2 access_flags;//表示字段的访问权限和基本属性
u2 name_index;//对常量池的一个有效索引,表示一个有效的非限定名
u2 descriptor_index;//对常量池的一个有效索引,表示一个有效的字段描述符
u2 attributes_count;//表示当前字段的附加属性的数量
attribute_info attributes[attributes_count];//属性的具体信息,其值为attribute_info结构
}
方法
所有的方法,包括实例初始化方法()和类或接口初始化方法()在内,都由method_info结构来定义。
method_info结构格式如下:
method_info {
u2 access_flags;//用于定义当前放法的访问权限和基本属性
u2 name_index;//常量池的一个有效的索引,要么表示一个特殊的方法,要么表示一个方法的有效非限定名。
u2 descriptor_index;//常量池的一个有效索引,表示一个有效的方法的描述符
u2 attributes_count;//表示当前方法附加属性的数量
attribute_info attributes[attributes_count];//属性的值,且每一项值都需要是attribute结构。
}
属性
属性在class文件中ClassFile结构、field_info结构、method_info结构和Code_attribute结构都有使用。(这个属性太多了,我就只简单列举几个了,想继续研究的话可以再查查相关文档)
所有属性的通用格式如下:
attribute_info {
u2 attribute_name_index;//对常量池的有效索引,表示当前属性的名字;
u4 attribute_length;//给出了跟随其后的信息字节的长度
u1 info[attribute_length];//信息的内容
}
下面给出一些常见的属性:
- ConstantValue属性
这个属性是定长属性,位于field_info结构的属性表中,ConstantValue表示一个常量表达式。 - Code属性
Code属性是变长属性,位于method_info结构的属性表中。Code属性中包含某个方法、实例初始化方法、类或接口初始化方法的Java虚拟机指令及相关辅助信息。 - StackMapTable属性
这个属性是变长属性,位于Code属性的属性表中。这个属性用在虚拟机的类型检查验证阶段。 - Exceptions属性
这个属性是变长属性,位于method_info结构的属性表中。Exceptions属性指出了一个方法可能抛出的受检异常。
还有许多的属性,就不一一列举了,有兴趣的可以查查相关文档。
格式检查
如果Java虚拟机准备加载某个class文件,那么它首先应该保证这个文件符合class文件的基本格式,这个过程称为格式检查。所需验证的事项有:
- 前四个字节必须是正确的魔数
- 能够辨识出来的所有属性都必须具备合适的长度
- class文件的内容不能缺失,尾部不能有多余的字节
- 常量池必须符合相关的各项约束
Java虚拟机代码约束
Java虚拟机将普通方法、实例初始化方法或类和接口的初始化方法的代码存储在class文件method_info结构里的Code属性的code数组中。这些代码需要满足静态约束和结构化约束。
静态约束是一系列用来定义文件是否编排良好的约束。
结构化约束是为了限定Java虚拟机指令之间的关系。
class文件校验
Java虚拟机会在链接的阶段对class文件进行校验以判断其是否满足必要的约束。链接期校验还有助于增强解释器的运行期执行性能。直观的理解,就是咱们定义了一些规则,虽然编译时检查通过了,谁知道是否准备运行时是否哪些规则遭到了破坏。因此需要重新校验下。比如,通过校验可以确保如下内容:
- 操作数栈不会发生上限或下限溢出
- 所有局部变量的使用和存储都是有效的
- 所有Java虚拟机指令都拥有正确的参数类型等。
这个校验还涉及到很多内容,但是咱们要明白,虽然class文件有了,但也不是直接就拿来用的,还是需要校验一下地。。
总结:上面主要简单介绍了一下class文件结构,希望大家能对class文件的理解有了进一步的认识。
愿大家共同进步!!