Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件屮包含了 Java虚拟机指令集和符号表以及若干其他辅助信息。基于安全方面的考虑,Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束,但任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为语言的产品交付媒介。例如,使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其他语言的编译器一样可以把程序代码编译成Class文件,虚拟机并不关心Class的来源是何种语言,Java提供的语言无关性如下:
Class文件结构
- 魔数:每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用就是确定这个文件是否是一个能被虚拟机接受的Class文件。这魔数很具有浪漫气息,为:0xCAFEBABE(咖啡宝贝?)
- 版本号:紧接着魔数的4个字节是Class文件的版本号:第5和第6字节是次版本号,第7和第8字节是主版本号。
- 常量池:版本号之后是常量池入口,可以理解为Class文件中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,常量池中主要存放字面量和符号引用,由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据代表常量池容量计数值。
- 访问标志:常量池结束之后,紧接着的2个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public/abstract类型;如果是类的话,是否被声明为final等。
- 索引集合:包括类索引、父类索引、接口索引
- 字段表集合:描述接口或类中声明的变量,但不包括方法内部的局部变量。根据描述符规则,基本类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,如表。
对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang. String[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String;”,一个整型数组“int[]”将被记 录为“[I”。标识字符
含 义
标识字符
含 义
B
基本类型byte
J
基本类型long
C
基本类型char
S
基本炎型short
D
基本类勒double
Z
基本类期boolean
F
基本类型float
V
特殊类型void
I
基本类型int
L
对象类型,如Ljava/lang/Object
用描述符来描述方法时,按照先参数列表,后返冋值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为 “()Ljava/lang/String;”,方法 int indexOf(char[] source,int sourceOffset,int sourceCount, char[] target,int targetOffset,int targetCount, int fromlndex)的描述 符为 “([CII[CIII)I。 - 方法表集合:代码在方法表中的属性集合“Code”属性集合中。方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”方法。
- 属性表集合:在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专用信息。
字节码指令
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码, Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。
字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构,由于限制了Java虚拟机操作码的长度为一个字节(即0〜255),这意味着指令集的操作码总数不可能超过256条;又由于Class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体数据的结构,如果要将一个16位长度的无符号整数使用两个无符号字节存储起来(将它们命名为bytel和byte2),那它们的值应该是这样的:(byte l << 8) | byte2。这种操作在某种程度上会导致解释执行字节码时损失一些性能。但这样做的优势也非常明显,放弃了操作数长度对齐,就意味着可以省略很多填充和间隔符号;用一个字节来代表操作码,也是为了尽可能获得短小精干的编译代码。这种追求尽可能小数据量、高传输效率的设计是由Java语言设计之初面向网络、智能家电的技术背景所决定的,并一直沿用至今。
方法调用和返回指令
- invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
- invokeinterface:用于调用接口方法,它会在运行吋搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
- invokespecial:用于调用一些需要特殊处理的实例方法,包括实例构造器<init>方法、私有方法和父类方法。
- invokestatic:用于调用类方法(static方法)。
- invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,与之相反,其他方法称为虚方法(除去final方法)。除了使用invokestatic、invokespecial调用的方法之外还有一种,就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择,又或者说多态选择的结果肯定是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法。解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。而分派(Dispatch)调用则可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。
方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括 iretum (当返回值是 boolean、byte、char、short 和 int 类型时使用)、lretum、fretum、dreturn 和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。
异常处理指令
在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用 throw语句显式抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java 虚拟机指令检测到异常状况时自动拋出。例如,在前面介绍的整数运算中,当除数为零时, 虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。
同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都 是使用管程(Monitor)来支持的。同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机 的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持。
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都 必须执行其对应的monhorexit指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时monitorenter和 monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit指令。