Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有任何分隔符。当遇到需要占用8位字节以上空间数据时,则会按照高位在前的方式分割成若干8位字节进行存储。Class文件结构只有两种数据类型:无符号数和表。
无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码的字符串。
表是有多个无符号数或者其他表作为数据项构成的复合数据类型,所有表习惯性以"_info"结尾。
Class文件格式
魔数与Class文件的版本
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件。Class文件的魔数是:0xCAFEBABE。
紧接着魔数的4个字节存储的是Class文件的版本号,di5和di6个字节是次版本号,第7和第8个字节是主版本号。
常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量不固定,所以入口处有一个u2类型的数据,表示常量池的容量,且容量计数从1开始而不是0开始,空余第0项,用于表达不需要引用常量池的情况,Class文件中其他集合类型容量计数都是从0开始。
常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descritor)
- 方法的名称和描述符
Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期的转话就无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
访问标志
常量池结束之后,紧接着的两个字段代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class文件是类还是接口,是否定义为public类型,是否定义为abstract,是否行为为final等。
访问标志
类索引、父类索引与接口索引集合
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合,Class文件中有这个三项数据来确定这个类的集成关系。类型索引用于确定这个类的全限定名,父类索引用于确定这个类的父类全限定名。由于Java不允许多继承,所以父类索引只有一个,除了java.lang.Object之外,所有Java类的父类索引不为0。接口索引集合用来描述这个类实现了哪些接口,按顺序从左到右排列在接口索引集合中。
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量和实例级变量,但不包括在方法内部声明的局部变量。描述的信息包括:字段的作用域、是实例变量还是类变量、可变性(final)、并发可见性(volatile修饰)、可否被序列化、字段数据类型、字段名称。
方法表集合
Class文件存储格式中对方法的描述与字段的描述几乎采用了完全一致的方式,方法表的结构如字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
属性表集合
与Class文件中其他数据项目严格要求顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重,任何人实现的编译器都可以想属性表中写入自定义属性信息,Java虚拟机运行时会忽略掉它不认识的属性。每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则完全自定义的,值需要通过一个u4的长度属性去说明属性值所占用的位数即可,一个符合规则的属性表应该满足下图所定义的结构:
属性表结构
1、Code属性
Java程序方法体中的代码经过javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合中,但并非所有的方法表都必须存在这个属性,如接口或者抽象类中的方法就不存在Code属性,Code属性结构如下图:
Code属性表结构
2.Exceptions属性
Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Exceptions),也就是方法描述是在throws关键字后面列举的异常。
3、LineNumberTable属性
该属性用于描述Java源文件行号与字节码行号(字节码偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以选择不生成,如果选择不生成,当抛出异常是,堆栈中将不显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
4、LocalVariableTable属性
该属性用于描述栈帧中局部变量表中的变量与Java源码定义的变量之间的关系,也不是运行时必需属性,可以不用生成,当没有生成该属性时,其他人引用这个方法时将丢失所有参数名,默认用arg0、arg1之类的占位符替换原有的参数名。
5、SourceFile属性
该属性用于记录生成这个Class文件的源文件名称。这个属性也是可选的,如果不生成这个属性,当抛出异常是,堆栈中将不会显示出错误代码所属的文件名。
6、ConstantValue属性
该属性的作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可以使用这项属性。
7、InnerClasses属性
该属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它及它所包含的内部类生成InnerClasses属性。
8、Decprecated及Synthetic属性
两个属性都属于标志类型的布尔属性,只有存在和没有的区别,Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定位不再推荐使用,它可以通过代码中使用@deprecated注释进行设置。Synthtic属性代表此字段或者方法并不是由Java源码直接生成的,而是由编译器自行添加的。
参考资料
- 深入理解Java虚拟机 JVM高级特性与最佳实践 第2版
网友评论