整理自《深入理解Java虚拟机》(第2版)
类文件结构
一、JVM的平台无关性和语言无关性
Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。
Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力会比Java语言本身更加强大。因此,有一些Java语言本身无法有效支持的语言特性不代表字节码本身无法有效支持,这也为其他语言实现一些有别于Java的语言特性提供了基础。
二、Class类文件的结构
任何一个Class文件都对应着唯一一个类或接口的定义信息,但是类或接口并不一定都得定义在文件里(譬如类或接口可以通过类加载器直接生成)。这里通俗地将任意一个有效的类或接口应当满足的格式称为“Class文件格式”,实际上它并不一定以磁盘文件的形式存在。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式(Big-Endian)分割成若干个8为字节进行存储。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数与表。
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常回使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
Class的结构不像XML等描述语言,由于它没有任何分割符号,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
(1)魔数与Class文件的版本
魔数
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别,例如图片格式,gif或jpeg等在文件头中都存在魔数。使用魔数而不是扩展名来识别主要是基于安全方面的考虑,因为文件扩展名可以随意改动,文件格式的制定者可以*选择魔数值。Class文件的魔数值为:OxCAFEBABE。
版本号
紧接着魔数的4个字节存储的是Class文件的版本号:第5、6个字节是此版本好Minor Version,第7、8个字节是主版本号Major Version。
(2)常量池
在主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。这个容量计数是从1而不是0开始的。将0项常量空出来是有特殊考虑的,目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0表示。
Class文件结构中只有常量池的容量计数是从开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始的。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Description)
- 方法的名称和描述符
Java代码在进行Javac编译的时候,并不像C和C++那样有连接的步骤,而是在虚拟机加载Class文件的时候进行动态链接。也就是,Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法被虚拟机使用。当虚拟机运行时,需要从常池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池中每一项常量都是一个表,在JDK1.7之前共有11种结构各不相同的表结构数据,在JDK1.7中为了更好地支持动态语言调用,又增加了3种类型。这14种表都有一个共同的特点,就是表的开始的第一位是一个u1类型的标志位(tag),代表当前这个变量属于那种常量类型。这14种常量类型各自均有自己的结构。
(3)访问标志
常量池结束后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息。包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型等。
一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位一律为0.
(4)类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java不允许多继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都是父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0.接口索引集合描述类实现了哪些接口。
(5)字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,不包括方法内部声明的局部变量。Java中描述一个字段可以包含什么信息?例如字段的作用域(public,private,protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)等等,这些修饰符都是布尔值,适合用标志位表示。而字段叫什么名字、字段是什么数据类型,无法固定,只能引用常量池中的常量来描述。
(6)方法表集合
Class文件存储格式中对方法的描述与对字段的描述集合采用完全一致的方式,方法表的结构包括访问标志、名称索引、描述符索引、数据表集合等。
(7)属性表集合
在Class文件、字段表、方法表都可以携带自己的属性表结合,以用于描述某些场景专有的信息。与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合不要求有严格的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
三、字节码指令
JVM的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的0至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,而有一个操作码。
字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构,由于限制了Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过256条;又由于Class文件格式放弃了编译后代码的操作数长度对其,意味着虚拟机处理超过一个字节的数据时,要在运行时从字节中重建出具体数据的结构,如果要将一个16位长度的无符号整数使用两个无符号字节存储起来(将它们命名为byte1和byte2),那它们的值应该是这样的:
(byte1<<8)|byte2
这种操作在某种程度上会导致解释执行字节码时损失一些性能。但优势也很明显,放弃了操作数长度对其,就可以省略很多填充和间隔符号;用一个字节来代表操作码,也是为了尽可能获得短小精干的编译代码。这种追求尽可能小数据量、高传输效率的设计是由Java语言设计之初面向网络、智能家电的技术背景所决定的。
如果不考虑异常处理的话,Java虚拟机的解释器可以使用下面的伪代码当做基本的执行模型来理解,这个执行模型虽然简单,但依然可以有效工作:
do{
自动计算PC寄存器的值加1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if(字节码存在操作数)从字节码流中取出操作数;
执行操作码所定义的操作;
}while(字节码流长度>0)
字节码指令包括:
- 加载和存储指令
- 运算指令
- 类型转换指令
- 对象创建与访问指令
- 操作数栈管理指令
- 控制转移指令
- 方法调用和返回指令
- 异常处理指令
- 同步指令