类文件结构——深入理解Java虚拟机 笔记三
在之前的笔记中记录过,Java程序变成可执行文件的步骤是:源代码——>经过编译变成class文件——>经过JVM虚拟机变成可执行的二进制文件。因此,为了对JVM执行程序的过程有一个好的了解,我们需要先明白class文件到底是什么东西,它里面有那些信息以及如何存储的。
Class类文件结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件中。当遇到需要占用超过8位字节的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。
无符号数是基本的数据类型,以u1,u2,u4,u8分别表示1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据结构,所有表习惯性地以_info结尾。
我们先来看一下Class文件的格式,然后对这些数据一一进行讲解。
魔数和Class文件的版本
每个Class文件的头四个字节称为魔数,作用是来表示该文件是一个可以被虚拟机接受的Class文件。其作用类似于扩展名,但扩展名易更改,而魔数来进行类型判别比较安全。
紧接着的四个字节存储的是Class文件的版本号。第5、6字节为次版本号,第7、8字节为主版本号。
常量池
版本号之后的就是常量池。常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一。
常量池中主要存放两大类常量,字面量和符号引用。字面量比较接近于Java语言的常量概念,比如文本字符串、被声明为final的常量值等等而符号引用属于编译原理方面的概念,包括了下面三类常量:1、类和接口的全限定名 2、字段的名称和描述符 3、方法的名称和描述符
Java代码进行编译的时候,没有C那样用"链接"这一步骤,而是在虚拟机加载Class文件的时候进行动态链接。也就是Class文件不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的。当虚拟机运行的时候,需要从常量池获得对应的符号引用,再在类创建或运行的时候解析并翻译到具体内存地址中 ??
对这些一一列举也没有什么意义,如果需要某一项的内容,可以单独查找。本章我认为更关键的是对Class文件怎么生成的,怎么运作的理解。
在常量池中,记载了各种常量(包括系统执行时内部需要的)、方法表(用来记录该类的所有方法)、属性表等。他们的运作方法大同小异:通过已经定义的常量(包括字符串常量、方法名之类的信息)来填充对应的表信息,比如方法表中,我们需要得到方法名、返回值、参数等等信息,这些信息都存储在常量池中,只需要引用到对应位置即可表示。而方法表中更为关键的信息就是Code属性了,Code属性表示了该方法的执行过程。
先看一下Code属性表的内容:
里面像attribute_name_index之类都是索引,指向常量池中的一个字符串来表示对应的数据常量。关键的地方是code_length和后面的code,他们用来存储Java源代码编译后生成的字节码指令。code_length代码字节码长度,code用于存储字节码指令的一系列字节流,每一个指令都是一个u1类型的单字节。,当虚拟机读到对应字节码的字节后,他会查询出是什么指令并执行。
通过指令的分析,我们可以看到,通过字节码我们可以实现利用常量池中的数据信息,来分析运行结果并执行的。
在这个字节码中,它执行的数据交换、方法调用都是基于操作栈进行的,比如2A将对象移动到栈顶,B7是以栈顶对象为接受者,他们都是对一个栈进行的操作,但又有像invokespecial这样带参数的指令,这就与单纯使用堆栈进行操作不相符,因此另一个问题是JVM如何堆字节码执行。
直接使用javap -verbose 类的class文件 即可得到该class的内容。