Java虚拟机规范
平台无关性(一次编写,到处运行):运行在各种不同硬件平台和操作系统上的Java虚拟机都在可以载入和执行同一种平台无关的字节码,从而实现程序的“一次编写,到处运行”。
任何一个Class文件都对应着唯一的一个类或者接口的定义信息(但是反过来说,类或者接口并不一定都定义在文件中(譬如类和接口也可以动态生成,直接送入类加载器))。
Class文件是一组以8个字节为基础单位的二进制流,各个项目按照严格的顺序紧凑地排列在文件,中间没有任何分隔符。
Class文件由两种数据结构组成
之后介绍用到了下面这段TestClass.java
的类文件
public class TestClass {
private int m;
public int inr(){
return m + 1;
}
}
每个Class文件的头4个字节成为魔数(magic number),它的唯一作用时确定该文件是否是一个能被虚拟机接受执行的class文件。 Class文件的魔数是:CAFEBABE(咖啡宝贝)。
紧接着魔术的4个字节存储的是Class文件的版本号:第5、6个字节存储的是次版本号(Minor Version),第7、8个字节存储的是主版本号(Major Version)。
《Java虚拟机规范》明确要求虚拟机必须拒绝执行超过其版本号的Class文件,也就是Java虚拟机只能向下兼容以前的版本号,不能允许以后版本的Class文件。
紧接着主、次版本号的是常量池入口,常量池可以比作Class文件的资源仓库。
常量池入口处放置了一项u2类型的数据,表示常量池容量计数值。并且计数从1开始,例如十六进制0x0016,即十进制的22,表示常量池中有21项常量(#1~#22)。#0表示不引用常量池中的任何项目。
常量池中有两大类常量
常量池中每一项都是一个表,截至JDK13,共有17种不同类型的常量。它们的共同特点是:表结构起始第一位是个u1类型的标志位(tag,取值是下表的标志位),代表着当前常量属于哪一种常量类型
表中没有列出的为了支持模块化系统的两项为:
CONSTANT_Class_Info类型:
CONSTANT_Utf8_Info类型:
由于length是u2类型,最大值为65535,所以Java中的英文字符的变量名和方法名最大为64KB,如果超出这个范围编译会报错。其他类型不再一一介绍,它们的结构如下表所示:
使用javap -verbose TestClass
命令可以查看并分析类文件,下面为类文件中常量池的部分:
在常量池结束之后,紧接着的2个字节表示访问标志(access_flags),这个标识用于识别一些类或者接口的访问信息,包括:这个class是类还是接口;是否为public类;是否为abstract类型;如果是类的话,是否被声明为final。具体含义以及标志值如下表所示:
补充:ACC_MODULE:0x8000,标识这是一个模块
类索引(this_class)和父类索引(super_class)都是一个U2类型的数据,而接口索引集合(interfaces)是一组u2类型数据的集合。
类索引用于确定这个类的全限定类名,夫类索引用于确定这个类的父类的全限定类型。他们都用一个u2类型的索引值表示,他们各自指向一个类型为CONSTANT_Class_Info类型的类描述符常量,通过CONSTANT_Class_Info类型可以找到定义在CONSTANT_Utf8_Info类型的常量中的全限定类名。
接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口按照implement关键字(或者extends)后的接口顺序从左到右排列在接口索引集合中
字段表(field_info)用于描述接口或者类中声明的变量,包括类级变量以及实例级变量,但不包括方法内部声明的局部变量。字段便结构如下表所示:
字符修饰符放在access_flags中,是一个u2的数据类型,内容如下表所示
跟随access_flags标志的是两项索引值:字段简单名称索引(name_index,例如inc, m),字段和方法描述符(descriptor_index,例如:()V、[Ljava/lang/String)。(详见书228)。
最后跟随着一个属性表集合,用于存储一些额外的信息,字段可以在属性表中附加描述零至多项额外信息。
方法表的结构和字段表一样,依次包含访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表集合(attributes)
对于方法表,所有的访问标志位及其取值如下表所示:
Class文件、字段表、方法表都可以携带自己的属性表集合以描述某些场景的专有信息。
具体内容参考《深入理解JVM》 第三版,230页。
Java虚拟机的字节码是由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode),以及后面跟随的零至多个代表此操作操作所需的参数(称为操作数,Operand)构成。
由于Java虚拟机操作码的长度为一个字节(即0~ 255), 这意味着指令集的操作码总数不型够超过256条;又由于Class文件格式放弃了编译后代码的操作数长度对齐。
对于大部分与数据相关的指令,他们的操作码助记符中都含有特殊字符来表明专门为哪一种数据类型服务:i表示int;l表示long;s表示short;b表示byte;c表示char;f表示float;d表示double;a表示reference
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈(见第2章关于内存区域的介绍)之间来回传输。
其中以尖括号结尾的这些助记符都代表了一组指令,例如iload_
代表iload_0
、iload_1
、iload_2
、iload_3
这几条指令,这样做的原因是可以省略操作数。
算术指令用于对操作数栈上的两个值进行某种特定运算,并把结果从新存入操作数栈
特点:
类型转换指令可以将两种不同类型的数据相互转换。
1、宽化类型转换,即小范围类型转化为大范围的安全转换。这些转换无需显式命令:
2、窄化类型转换,需要显式的指令进行转换,这些命令包括i2b, i2c, i2s, l2i, f2i, d2i, f2l, d2l, d2f。窄化类型转换可能导致结果正负号转变、不同的数量级请求的情况,转化过程很可能会导致数据精度丢失
下面的指令包括对对象的创建(其中类实例和数组的创建和操作采用了不同的指令),和对对象的相关访问操作:
如同操作一个普通数据结构中的堆栈一样,Java虚拟机提供了一些用于直接操作操作数栈的指令:
控制转移指令可以让Java虚拟机有条件或无条件地从指定位置指令(而不是控制转移指令)的下一多指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存器的值。控制转移指令包括:
方法调用(分派、执行过程)的几下指令如下图所示:
在Java程序中显示抛出异常的操作(throw语句)都由athrow指令完成。处理异常(cache语句)不是由字节码指令来实现的,而是采用异常表实现。
Java虚拟机可以支持方法级别的同步和方法内部一段指令序列的同步,两种同步结构都是使用管程(Monitor,更常见的是直接称之为锁)来实现。