JVM学习笔记(4)-JVM类文件结构
简介
Class文件是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础构成之一。了解Class文件的结构对后面进一步了解虚拟机执行引擎很有重要的意义。
我们的知道,Java是跨平台的语言。在Java发展之初,设计者们就考虑过了在Java虚拟机上运行其它语言的可能性。时至今日商业机构和开源机构以及在Java语言之外发展出一大批在Java虚拟机上运行的语言,如Clojure,Groovy,JRuby,Jython,Scala,等等。实现语言无关性的基础仍然是虚拟机和字节码存储格式,使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其它语言的编译器一样可以把程序编译成Class文件,虚拟机不需要关心Class来源于什么语言,只要它符合Class文件应用的结构就可以在Java虚拟机中运行。
class类文件的结构
Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。整个Class文件本质上就是一张表。
占用大小 | 字段描述 | 数量 |
---|---|---|
4bit | magic:魔数,用于标识文件类型,对于java来说是0xCAFEBABE | 1 |
2bit | minor_version:次版本号 | 1 |
2bit | major_version:主版本号 | 1 |
2bit | constant_pool_count:常量池大小,从1开始而不是0。当这个值为0时,表示后面没有常量 | 1 |
不定 | constant_pool:#常量池 | constant_pool_count-1 |
2bit | access_flags:访问标志,标识这个class是类还是接口、public、abstract、final | 1 |
2bit | this_class:类索引 | 1 |
2bit | super_class:父类索引 | 1 |
2bit | interfaces_count:接口计数器 | 1 |
每个2bit | interfaces:接口索引集合 | interfaces_count |
2bit | fields_count:字段的数量 | 1 |
不定 | fields:#字段表 | fields_count |
2bit | methods_count:方法数量 | 1 |
不定 | methods:#方法表 | methods_count |
2bit | attributes_count:属性数量 | 1 |
不定 | attrbutes:#属性表 | attributes_count |
我们可以使用WinHex来查看class文件
class示例图
魔数与class文件的版本
每个Class文件头4个字节称为魔数,它的唯一作用是确定这个文件是否是一个能被虚拟机接受的Class文件。
根据class示例图可以看出魔数值(4个字节)为0xCAFEBABE;紧接着第5和第6个字节是次版本号,第7、8个字节是主版本号。这里需要注意高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。
常量池
常量池可以理解为Class文件之中的资源仓库。常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符合引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用、再在类创建时或运行时解析、翻译到具体的内存地址之中
访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口:是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被生命为final等
类索引、父类索引与接口索引集合
类索引和分类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合,Class文件中由三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。对于接口索引集合,入口的第一项u2类型的数据为接口计数器,表示索引表的容量。
字段表集合
字段表用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段表中字段的各种描述信息(作用域比如public,private,是否被final,static修饰,是否可序列化等)均使用标志位表示,名称则引用常量池中的常量来描述。
方法表集合
在方法表中,方法的描述和字段的描述基本一致,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
方法中的代码经过编译器编译成字节码指令后存放在方法属性表集合中一个名为“Code”的属性里面。
如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。
属性表集合
Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。为了能正确解析Class文件,在Java SE 7中预定义了21项属性,虚拟机在运行时会忽略他不认识的属性。