java在创建的初中是一次编写,可以在多平台运行,这与c++等有一些区别,因为c++这些在不同的平台代码可能不是通用的,java主要得益于java虚拟机,保证了平台无关性,这种保证是依靠字节码class文件实现的,虚拟机主要是识别class,将class解释成机器语言执行,是不是我们只要可以将代码转换成字节码都能依靠虚拟机最终执行呢?这就是语言无关性。

时至今日,商业企业和开源机构已经在Java语言之外发展出一大批运行在Java虚拟机之上的语言,

如Kotlin、Clojure、Groovy、JRuby、JPython、Scala等。

实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不与包括Java语言在内的任何

程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机

指令集、符号表以及若干其他辅助信息。基于安全方面的考虑,作为一个通用的、与机器无关的执行平

台,任何其他语言的实现者都可以将Java虚拟机作为他们语言的运行基础,以Class文件作为他们产品

的交付媒介。

Java语言中的各种语法、关键字、常量变量和运算符号的语义最终都会由多条字节码指令组合来

表达,这决定了字节码指令所能提供的语言描述能力必须比Java语言本身更加强大才行。因此,有一

些Java语言本身无法有效支持的语言特性并不代表在字节码中也无法有效表达出来,这为其他程序语

言实现一些有别于Java的语言特性提供了发挥空间。

魔数与Class文件的版本

每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为

一个能被虚拟机接受的Class文件。不仅是Class文件,很多文件格式标准中都有使用魔数来进行身份识

别的习惯,譬如图片格式,如GIF或者JPEG等在文件头中都存有魔数。使用魔数而不是扩展名来进行

识别主要是基于安全考虑,因为文件扩展名可以随意改动。文件格式的制定者可以自由地选择魔数

值,只要这个魔数值还没有被广泛采用过而且不会引起混淆。

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor

Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后

的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能

向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,因为《Java虚拟机规范》在Class文

件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class

文件。

常量池

紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class

文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它

还是在Class文件中第一个出现的表类型数据项目。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比

较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译

原理方面的概念,主要包括下面几类常量:

·被模块导出或者开放的包(Package)

·类和接口的全限定名(Fully Qualified Name)

·字段的名称和描述符(Descriptor)

·方法的名称和描述符

·方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)

·动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class

文件的时候进行动态连接。在Class文件中不会保存各个方法、字段最终

在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号

引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池中每一项常量都是一个表,最初常量表中共有11种结构各不相同的表结构数据,后来为了

更好地支持动态语言调用,额外增加了4种动态语言相关的常量[1],为了支持Java模块化系统

(Jigsaw),又加入了CONSTANT_Module_info和CONSTANT_Package_info两个常量,所以截至JDK

13,常量表中分别有17种不同类型的常量。

访问标志

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或

者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract

类型;如果是类的话,是否被声明为final;等等。

类索引、父类索引与接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合

(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。类索

引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多

重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了

java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接

口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是

extends关键字)后的接口顺序从左到右排列在接口索引集合中。

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变

量以及实例级变量,但不包括在方法内部声明的局部变量。读者可以回忆一下在Java语言中描述一个

字段可以包含哪些信息。字段可以包括的修饰符有字段的作用域(public、private、protected修饰

符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否

强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、

字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标

志位来表示。而字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常

量池中的常量来描述。

方法表集合

Class文件存储

格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依

次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表

集合(attributes)几项。

方法的定义可以通过访问标志、名称索引、描述符索引来

表达清楚,但方法里面的代码去哪里了?方法里的Java代码,经过Javac编译器编译成字节码指令之

后,存放在方法属性表集合中一个名为“Code”的属性里面。

与字段表集合相对应地,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出

现来自父类的方法信息。但同样地,有可能会出现由编译器自动添加的方法,最常见的便是类构造

器“<clinit>()”方法和实例构造器“<init>()”方法[1]

Java代码的方法特征签名只包括方法名称、参数

顺序及参数类型,而字节码的特征签名还包括方法返回值以及受查异常表,所以java的重载是不可以靠返回值区分的,而class是可以支持的。

属性表集合

属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以

携带自己的属性表集合,以描述某些场景专有的信息。

Code属性

Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。

Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽

象类中的方法就不存在Code属性。

max_stack代表了操作数栈(Operand Stack)深度的最大值。在方法执行的任意时刻,操作数栈都

不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。

max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是变量槽(Slot),变量

槽是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和

returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64

位的数据类型则需要两个变量槽来存放。

并不是在方法中用了多少个局部变量,就把这

些局部变量所占变量槽数量之和作为max_locals的值,操作数栈和局部变量表直接决定一个该方法的栈

帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费。Java虚拟机的做法是将局

部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量

槽可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配变量槽给各个变量使用,根据

同时生存的最大局部变量数量和类型计算出max_locals的大小。

是《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令,如果超过这个限制,Javac编译器就会拒绝编译。一般来讲,编写Java代码时

只要不是刻意去编写一个超级长的方法来为难编译器,是不太可能超过这个最大值的限制的。但是,

某些特殊情况,例如在编译一个很复杂的JSP文件时,某些JSP编译器会把JSP内容和页面输出的信息归

并于一个方法之中,就有可能因为方法生成字节码超长的原因而导致编译失败。

下面args_size代表参数数量,locals代表局部变量表变量数量,stack代表方法深度

args_size=1尽管没有传参但还是1是因为编译后实例方法会默认传this指针参数,局部变量表也会分一个槽给this,如果是static方法就没有。

// 原始Java代码
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
C:\>javap -verbose TestClass
// 常量表部分的输出见代码清单6-1,因版面原因这里省略掉
{
public org.fenixsoft.clazz.TestClass();
Code:
Stack=1, Locals=1, Args_size=1
0: aload_0
1: invokespecial #10; //Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/fenixsoft/clazz/TestClass;
public int inc();
Code:
Stack=2, Locals=1, Args_size=1
0: aload_0
1: getfield #18; //Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lorg/fenixsoft/clazz/TestClass;

异常表:

异常表实际上是Java代码的一部分,尽管字节码中有最初为处理异常而设计的跳转指令,但《Java

虚拟机规范》中明确要求Java语言的编译器应当选择使用异常表而不是通过跳转指令来实现Java异常及

finally处理机制[2]

// Java源码
public int inc() {
	int x;
	try {
		x = 1;
		return x;
	} catch (Exception e) {
		x = 2;
		return x;
	} finally {
		x = 3;
	}
}
// 编译后的ByteCode字节码及异常表
public int inc();
Code:
Stack=1, Locals=5, Args_size=1
0: iconst_1 // try块中的x=1
1: istore_1
2: iload_1 // 保存x到returnValue中,此时x=1
3: istore 4
5: iconst_3 // finaly块中的x=3
6: istore_1
7: iload 4 // 将returnValue中的值放到栈顶,准备给ireturn返回
9: ireturn
10: astore_2 // 给catch中定义的Exception e赋值,存储在变量槽 2中
11: iconst_2 // catch块中的x=2
12: istore_1
13: iload_1 // 保存x到returnValue中,此时x=2
14: istore 4
16: iconst_3 // finaly块中的x=3
17: istore_1
18: iload 4 // 将returnValue中的值放到栈顶,准备给ireturn返回
20: ireturn
21: astore_3 // 如果出现了不属于java.lang.Exception及其子类的异常才会走到这里
22: iconst_3 // finaly块中的x=3
23: istore_1
24: aload_3 // 将异常放置到栈顶,并抛出
25: athrow
Exception table:
from to target type
0 5 10 Class java/lang/Exception
0 5 21 any
10 16 21 any

编译器为这段Java源码生成了三条异常表记录,对应三条可能出现的代码执行路径。从Java代码的

语义上讲,这三条执行路径分别为:

·如果try语句块中出现属于Exception或其子类的异常,转到catch语句块处理;

·如果try语句块中出现不属于Exception或其子类的异常,转到finally语句块处理;

·如果catch语句块中出现任何异常,转到finally语句块处理。

如果没有出现异常,返回值是1;如果出现了Exception异常,返回值是2;如果出现了

Exception以外的异常,方法非正常退出,没有返回值。

字节码中第0~4行所做的操作就是将整数1赋值给变量x,并且将此时x的值复制一份副本到最后一

个本地变量表的变量槽中(这个变量槽里面的值在ireturn指令执行前将会被重新读到操作栈顶,因为是复制所以finally里的赋值不会影响返回结果。作为方法返回值使用。为了讲解方便,笔者给这个变量槽起个名字:returnValue)。如果这时候没有出现异

常,则会继续走到第5~9行,将变量x赋值为3,然后将之前保存在returnValue中的整数1读入到操作栈

顶,最后ireturn指令会以int形式返回操作栈顶中的值,方法结束。如果出现了异常,PC寄存器指针转

到第10行,第10~20行所做的事情是将2赋值给变量x,然后将变量x此时的值赋给returnValue,最后再

将变量x的值改为3。方法返回前同样将returnValue中保留的整数2读到了操作栈顶。从第21行开始的代

码,作用是将变量x的值赋为3,并将栈顶的异常抛出,方法结束。

LineNumberTable属性

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。

它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中使用-g:none或-g:lines

选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要影

响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来

设置断点。

字节码执行计算操作的时候会根据操作数在操作数栈计算,操作数占一个字节,也就是8位从0-255 总共256个种类,为了在有限的种类里表示所有的操作数,会把一部分进行转换,比如把bool char byte等当做int计算。

字节码与数据类型

在Java虚拟机的指令集中,大多数指令都包含其操作所对应的数据类型信息。举个例子,iload指

令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。这两

条指令的操作在虚拟机内部可能会是由同一段代码来实现的,但在Class文件中它们必须拥有各自独立

的操作码。

对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为

哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表

float,d代表double,a代表reference。也有一些指令的助记符中没有明确指明操作类型的字母,例如

arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。还有另

外一些指令,例如无条件跳转指令goto则是与数据类型无关的指令。

大部分指令都没有支持整数类型byte、char和short,甚至没有任何指令

支持boolean类型。编译器会在编译期或运行期将byte和short类型的数据带符号扩展(Sign-Extend)为

相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据。与之类

似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来

处理。因此,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的对int类

型作为运算类型(Computational Type)来进行的。

类型转换

在将int或long类型窄化转换为整数类型T的时候,转换过程仅仅是简单丢弃除最低位N字节以外的

内容,N是类型T的数据类型长度,这将可能导致转换结果与输入值有不同的正负号。对于了解计算机

数值存储和表示的程序员来说这点很容易理解,因为原来符号位处于数值的最高位,高位被丢弃之

后,转换结果的符号就取决于低N字节的首位了。

Java虚拟机将一个浮点值窄化转换为整数类型T(T限于int或long类型之一)的时候,必须遵循以

下转换规则:

·如果浮点值是NaN,那转换结果就是int或long类型的0。

·如果浮点值不是无穷大的话,浮点值使用IEEE 754的向零舍入模式取整,获得整数值v。如果v在

目标类型T(int或long)的表示范围之类,那转换结果就是v;否则,将根据v的符号,转换为T所能表

示的最大或者最小正数。

·如果浮点值不是无穷大的话,浮点值使用IEEE 754的向零舍入模式取整,获得整数值v。如果v在

目标类型T(int或long)的表示范围之内,那转换结果就是v;否则,将根据v的符号,转换为T所能表

示的最大或者最小正数。(只有浮点数转整数会这样,其余还是二进制截断)。

从double类型到float类型做窄化转换的过程与IEEE 754中定义的一致,通过IEEE 754向最接近数舍

入模式舍入得到一个可以使用float类型表示的数字。如果转换结果的绝对值太小、无法使用float来表

示的话,将返回float类型的正负零;如果转换结果的绝对值太大、无法使用float来表示的话,将返回

float类型的正负无穷大。对于double类型的NaN值将按规定转换为float类型的NaN值。

对象创建与访问指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指

令(在下一章会讲到数组和普通类的类型创建过程是不同的)。对象创建后,就可以通过对象访问指

令获取对象实例或者数组实例中的字段或者数组元素,这些指令包括:

·创建类实例的指令:new

·创建数组的指令:newarray、anewarray、multianewarray

·访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的

指令:getfield、putfield、getstatic、putstatic

·把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、

daload、aaload

·将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、

dastore、aastore

·取数组长度的指令:arraylength

·检查类实例类型的指令:instanceof、checkcast

管程 (Moniters,也称为监视器)

一.管程的概念

是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。

与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。

管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。即:在管程中的线程可以临时放弃管程的互斥访问,让其他线程进入到管程中来。

管程包含:

多个彼此可以交互并共用资源的线程

多个与资源使用有关的变量

一个互斥锁

一个用来避免竞态条件的不变量

一个管程的程序在运行一个线程前会先取得互斥锁,直到完成线程或是线程等待某个条件被满足才会放弃互斥锁。若每个执行中的线程在放弃互斥锁之前都能保证不变量成立,则所有线程皆不会导致竞态条件成立。

管程是一种高级的同步原语。任意时刻管程中只能有一个活跃进程。它是一种编程语言的组件,所以编译器知道它们很特殊,并可以采用与其他过程调用不同的方法来处理它们。典型地,当一个进程调用管程中的过程,前几条指令将检查在管程中是否有其他的活跃进程。如果有,调用进程将挂起,直到另一个进程离开管程。如果没有,则调用进程便进入管程。

对管程的实现互斥由编译器负责!在Java中,只要将关键字synchronized加入到方法声明中,Java保证一旦某个线程执行该方法,就不允许其他线程执行该方法,就不允许其他线程执行该类中的任何其他方法。

注意:管程是一个编程语言概念。编译器必须要识别出管程并用某种方式对互斥做出安排。C、Pascal及多数其他语言都没有管程,所以指望这些编译器来实现互斥规则是不可靠的。

管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。

进程只能互斥得使用管程,即当一个进程使用管程时,另一个进程必须等待。当一个进程使用完管程后,它必须释放管程并唤醒等待管程的某一个进程。

在管程入口处的等待队列称为入口等待队列,由于进程会执行唤醒操作,因此可能有多个等待使用管程的队列,这样的队列称为紧急队列,它的优先级高于等待队列。

二、 管程的特征

1. 模块化。

管程是一个基本的软件模块,可以被单独编译。

2. 抽象数据类型。

管程中封装了数据及对于数据的操作,这点有点像面向对象编程语言中的类。

3. 信息隐藏。

管程外的进程或其他软件模块只能通过管程对外的接口来访问管程提供的操作,管程内部的实现细节对外界是透明的。

4. 使用的互斥性。

任何一个时刻,管程只能由一个进程使用。进入管程时的互斥由编译器负责完成。

三、 enter过程、leave过程

1. enter过程

一个进程进入管程前要提出申请,一般由管程提供一个外部过程--enter过程。如Monitor.enter()表示进程调用管程Monitor外部过程enter进入管程。

2. leave过程

当一个进程离开管程时,如果紧急队列不空,那么它就必须负责唤醒紧急队列中的一个进程,此时也由管程提供一个外部过程—leave过程,如Monitor.leave()表示进程调用管程Monitor外部过程leave离开管程。

同步指令

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管

程(Monitor,更常见的是直接将它称为“锁”)来实现的。

方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟

机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为

同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如

果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成

还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取

到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同

步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。

同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中

有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字

需要Javac编译器与Java虚拟机两者共同协作支持。

void onlyMe(Foo f) {
	synchronized(f) {
		doSomething();
	}
}
Method void onlyMe(Foo)
0 aload_1 // 将对象f入栈
1 dup // 复制栈顶元素(即f的引用)
2 astore_2 // 将栈顶元素存储到局部变量表变量槽 2中
3 monitorenter // 以栈定元素(即f)作为锁,开始同步
4 aload_0 // 将局部变量槽 0(即this指针)的元素入栈
5 invokevirtual #5 // 调用doSomething()方法
8 aload_2 // 将局部变量Slow 2的元素(即f)入栈
9 monitorexit // 退出同步
10 goto 18 // 方法正常结束,跳转到18返回
13 astore_3 // 从这步开始是异常路径,见下面异常表的Taget 13
14 aload_2 // 将局部变量Slow 2的元素(即f)入栈
15 monitorexit // 退出同步
16 aload_3 // 将局部变量Slow 3的元素(即异常对象)入栈
17 athrow // 把异常对象重新抛出给onlyMe()方法的调用者
18 return // 方法正常返回
Exception table:
FromTo Target Type
4 10 13 any
13 16 13 any

编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须有其对

应的monitorexit指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时monitorenter和monitorexit指

令依然可以正确配对执行,编译器会自动产生一个异常处理程序,这个异常处理程序声明可处理所有

的异常,它的目的就是用来执行monitorexit指令。

公有设计,私有实现

《Java虚拟机规范》描绘了Java虚拟机应有的共同程序存储格式:Class文件格式以及字节码指令

集。这些内容与硬件、操作系统和具体的Java虚拟机实现之间是完全独立的,虚拟机实现者可能更愿

意把它们看作程序在各种Java平台实现之间互相安全地交互的手段。

理解公有设计与私有实现之间的分界线是非常有必要的,任何一款Java虚拟机实现都必须能够读

取Class文件并精确实现包含在其中的Java虚拟机代码的语义。拿着《Java虚拟机规范》一成不变地逐

字实现其中要求的内容当然是一种可行的途径,但一个优秀的虚拟机实现,在满足《Java虚拟机规

范》的约束下对具体实现做出修改和优化也是完全可行的,并且《Java虚拟机规范》中明确鼓励实现

者这样去做。只要优化以后Class文件依然可以被正确读取,并且包含在其中的语义能得到完整保持,

那实现者就可以选择以任何方式去实现这些语义,虚拟机在后台如何处理Class文件完全是实现者自己

的事情,只要它在外部接口上看起来与规范描述的一致即可[1]

虚拟机实现者可以使用这种伸缩性来让Java虚拟机获得更高的性能、更低的内存消耗或者更好的

可移植性,选择哪种特性取决于Java虚拟机实现的目标和关注点是什么,虚拟机实现的方式主要有以

下两种:

·将输入的Java虚拟机代码在加载时或执行时翻译成另一种虚拟机的指令集;

·将输入的Java虚拟机代码在加载时或执行时翻译成宿主机处理程序的本地指令集(即即时编译器

代码生成技术)。

精确定义的虚拟机行为和目标文件格式,不应当对虚拟机实现者的创造性产生太多的限制,Java

虚拟机是被设计成可以允许有众多不同的实现,并且各种实现可以在保持兼容性的同时提供不同的新

的、有趣的解决方案。