用 Java 代码实现一个 Java 虚拟机和它的相关子系统(包括优化编译器)提出了几个挑战。Taivalsaari 36 也描述了一个“用 Java 写 Java”的 JVM 实现,设计它是为了检查用 Java 写的高质量的虚拟机的可行性。这个方案的一个缺点是它运行在另一台 JVM 上,这增加了性能开销,因为有两级解释过程。麻省理工学院(Massachusetts Institute of Technology(MIT))的 Rivet JVM 37 也运行在另一台 JVM 上面。通过自举系统,我们的方案不需要另一台 JVM(请参阅 附录 B)。IBM VisualAge for Java 38 的 JVM 是用 Smalltalk 写的。其它的 JVM 39 是用本机代码写的。
最令人兴奋的传统 JVM 可能是 HotSpot。 40 HotSpot 的对象模型和 Jalape�o 的有点相似:对象直接被引用(而不是通过句柄)而且对象有一个两个字的头。在两个模型中,关于对象的类的信息都通过对象头中的引用可用。
HotSpot 最初解释字节码,编译(并内联)被频繁调用的方法。Jalape�o 的快速编译器将扮演与 HotSpot 的解释器相似的角色。其它所有都是同等的,这对 HotSpot 会带来启动方面的好处,对 Jalape�o 会带来性能方面的好处。我们不期望哪个好处会特别大,但这仍然可以看到。如果未优化的 Jalape�o 代码比解释后的 HotSpot 代码表现更好,这将允许 Jalape�o 优化编译器把更多的资源集中在它优化的代码上。用 Java 代码实现 Jalape�o 允许优化编译器内联和优化频繁被调用的运行时服务,HotSpot 通过调用本机代码(极好地优化了的 C 例程)访问这些服务。
HotSpot 把 Java 线程作为宿主操作系统线程实现。这些线程是完全抢先的。Jalape�o 调度它自己的准抢先线程。我们希望这将允许支持更多的线程、更轻量的同步和使从正常操作到垃圾回收的更平滑切换(特别是在有极多线程的情况下)。HotSpot 的每线程方法激活堆栈符合宿主操作系统的调用约定。这应会给 Jalape�o 较小的空间和性能方面的好处(尽管 Jalape�oill 在它 确实调用 C 代码时获得了性能方面的好处)。
HotSpot 和 Jalape�o 都支持类型确切的垃圾回收。Jalape�o 支持一系列内存管理器。Jalape�o 的回收器没有一个象 HotSpot 的那么成熟,但在 SMP 上,Jalape�o 的回收器并行地运行在所有可用的 CPU 上。HotSpot 使用一个带有针对主要回收的“标记并压缩”繁衍方案。为尽可能减少暂停时间,HotSpot 可以使用一个增量的“火车(train)” 回收器。 44 这个回收器执行频繁的短回收。注意,这将加剧任何的过渡到回收(transition-to-collection)延迟。
我们没有关于 HotSpot 的锁定机制的信息。
Squeak 45 是一个用 Smalltalk 写的 Smalltalk 虚拟机。它通过把虚拟机转换为 C 以编译和链接接来生成产品版。转换程序也是用 Smalltalk 写的。
动态编译(称为动态转换或即时编译)已经成为很多面向对象语言的以前的实现中的一个关键因素。Deutsch 和 Schiffman 的 Smalltalk-80 的高性能实现动态地把 Smalltalk 字节码转换成本机代码; 46 他们的编译器与 Jalape�o 的基线编译器非常相似。Self 语言的实现也依赖于动态编译来达到高性能。 47 Self 编译使用与 Jalape�o 的优化编译器所用的中间表示大体相当的的基于寄存器的中间表示。最近,大量 Java 语言的即时编译器已被开发出来。 31 , 48 这些编译器中,有一些编译器把字节码转换成三地址代码,进行简单的优化和寄存器分配,然后就生成目标机器代码。
DAISY 49 是一个 VLIW(very long instruction word(超长指令字))仿真器,它“快速”地把不同体系结构指令集,包括 Java 字节码,转换成 VLIW 体系结构。它使用 VLIW 类似于树的表示以进行指令调度和寄存器分配。
很多以前的系统使用了动态编译的更专门的形式,以通过使用“运行时常数”来有选择地优化程序的“热点”。 50 一般来说,这些系统都强调极其快速的动态编译,经常执行大量的脱机预计算,以避免构造正在编译的程序片段的任何显式表示。
有大量的工作用于处理特定于面向对象语言的优化,例如类分析(过程内的 54 和过程间的 55 ),类层次结构分析和优化 56 , 57 ,接收器类预测 46 , 58 , 59 ,方法规范 56 和调用图构造。 55 其它与 Java 编译有关的优化包括边界检查清除 60 和语义扩展。 22
用于 Java 服务器的 Jalape�o 的虚拟机是用 Java 语言编写的。传统上用本机方法支持的运行时服务主要地用 Java 代码实现。
Jalape�o 的对象布局支持单指令字段访问,对数组元素的三指令访问,硬件空指针检查和四指令虚方法调度。通过全局 JTOC 数组快速访问静态字段和方法也实现了。
Jalape�o 的线程通过虚拟处理器进行多路复用。线程切换是准抢先的。三个不同的锁定机制提供了轻量级同步,而不用操作系统支持。
Jalape�o 的内存管理子系统支持一系列内存管理器,每个内存管理器由一个并发对象分配器和一个并行的、类型确切的、停住一切的垃圾回收器组成。繁衍的和不繁衍的,拷贝的和非拷贝的管理器都被支持。增量及并发回收器正在研究之中。
Jalape�o 的三个可互操作的编译器提供了不同级别的动态优化,确保了实时的线程抢先,并且生成了支持异常处理、堆栈中的调用的位置和调试的表。
Jalape�o 的优化编译器为已被识别为频繁执行或计算密集的方法产生优质代码。需被再次编译的方法将根据运行时配置动态地选择。
我们已经证明了用 Java 语言为 Java 服务器构建虚拟机的可行性。我们尚未证明这样一个虚拟机能达到并保持世界级的性能。我们正在为此努力。
为分配一个对象,Jalape�o 的内存管理器必须访问原始内存以获得要求大小的一块可用空间。它们“遍历”线程堆栈以识别堆栈帧中的对象引用。拷贝管理器在垃圾回收期间访问对象头以标记对象,访问原始内存以拷贝一个对象。异常处理要求非结构化的传送控制以转到适当的 catch块(“go to”在 Java 语言中是禁止的)。静态数据和方法通过一个专用的机器寄存器访问,不能从 Java 指令访问这个专用寄存器。输入和输出要求访问 Java 语言不知道的操作系统服务。线程切换依赖于接收到来自操作系统的周期性中断。Jalape�o 的锁定机制是用 PowerPC 指令实现的,这些指令无法表达成 Java 字节码。不打破 Java 的编程模型,这些操作都无法进行。
为实现 Jalape�o Java 代码,有必要增加 Java 的功能,以包含本机方法传统上要求的功能:
Jalape�o 必须要有这些功能,但 Jalape�o 也必须防止用户应用程序能使用这些功能。
在专门的 MAGIC 类的帮助下,Jalape�o 的编译器支持这些违例。这个类的方法符合 Java 外部操作,Jalape�o 必须能够执行这些操作。这些方法的体是空的。Java 的源代码编译器能够编译它们。但是,Jalape�o 的编译器忽略这些结果字节码。而且,这些编译器识别出 MAGICR 类的名字并内联地插入必需的机器代码。为确认用户代码未违背 Java 的约束,当 Jalape�o 的编译器碰到调用一个 MAGIC 方法时,它们将验证正在编译的方法是 JVM 的一个已授权的部分。
需要使用 MAGIC 类的代码在这样做时需特别小心。将要讲述的规则就是一个原因。某些操作要格外小心。涉及原始地址的计算尤其微妙。MAGIC 方法 objectAsAddress 把对象引用转换成原始地址(一个整数)。例如,在进行动态链接时就需要这个功能。然而,它也是有问题的。Jalape�o 的拷贝内存管理器在移动所引用的对象时会更新对象引用,但原始地址却未被更新。在进行涉及原始地址的计算时,为避免垃圾回收的发生,必须很小心,以免拷贝回收器使这些地址无效。这通过调用一个能禁用垃圾回收的方法来避免。
已经禁用了垃圾回收的线程不能试图创建一个对象,因为如果内存不足,系统将会挂起。(注意,其它线程可以自由申请内存。如果无法得到内存,则这些线程被延迟,而且一旦垃圾回收被重新启用,就将开始进行一个回收。)这个约束有一些微妙的牵连。类不能被装入,因为对象是在类装入期间创建的。这意味着必须避免动态链接。类型的强制转型(和存储到对象数组)也不允许,因为这可能也要求类装入。类似地,如果线程试图进入一个共享对象的一个管程,而这个管程当前正被一个正在等待垃圾回收的线程占有,那么系统将陷入死锁。因此,线程在进行涉及原始地址的计算时,必须严格限制在 Java 功能的子集内。
就线程当它的垃圾回收被禁用时进行让出(显式地或隐式地)来说,也会有点问题。这样一个让出可能会任意地延迟所需的垃圾回收。当线程的垃圾回收被禁用时,隐式线程切换被延迟(而显式线程切换被禁止)。
Jalape�o 系统中大约有 650 个 Java 类,其中大约有 110 个访问了 MAGIC 类。其中只有 12 个类要求禁用垃圾回收。
一组相当坚实的服务 ― 一个类装入器,一个对象分配器,一个编译器 ― 在 JVM 能够装入正常操作所需的所有剩余服务之前就必须已经存在。用本机代码编写的 JVM 的初始服务,或者运行在另一台 JVM 上的 JVM,都从底层运行时例程可用。Jalape�o 不是用本机代码编写的,它没有底层运行时例程。因此,我们把基本的核心服务装配进一个可执行 引导映象,这个引导映象先于 JVM 运行。这个引导映象是 Jalape�o 虚拟机的一个快照,它被写入到一个文件中。随后,这个文件被装入内存并执行。
引导映象由一个名为 引导映象编写器(boot-image writer)的 Java 程序创建。引导映象编写器构造运行中的 Jalape�o 虚拟机的实体模型(mock-up),然后把它包装进引导映象。引导映象编写器是一个普通的 Java 程序,它可以在任何 JVM 上运行。运行引导映象编写器的 JVM 将被称为 源 JVM,而产生的结果 Jalape�o 虚拟机则称为 目标JVM。
引导映象编写器类似于一个交叉编译器和链接器:它把字节码编译成机器码并重写机器地址以把程序组件绑定进可运行映象。然而,由于 Jalape�o 的编译器、类装入器和运行时数据结构都是 Java 代码形式,而不似多数编译器,所以引导映象编写器也必须把“活动”对象绑定进引导映象。
引导映象编写器在源 JVM 中实例化 Java 对象,这些对象表示了目标 JVM。然后,它使用 Java 内置的反射功能把这些实体模型对象从源 JVM 的对象模型转换为 Jalape�o 的对象模型。引导映象编写器和这种自引用特征使得它相对简单 ― 它实际上只是一个对象模型转换器。
由于 Jalape�o 是一个 Java 程序,所以它的每个组件都是一个 Java 对象,而且,通过在 Jalape�o 的每个主子系统中执行专门的初始化方法,引导映象编写器能够构造其实体模型。定制的类装入器确保了执行这些代码所需的任何类都既装入到了源 JVM 中,也装入到了实体模型中。当装入一个类时,类的方法被编译(由运行在源 JVM 中的 Jalape�o 的编译器执行)并被包含进实体模型。
要成功实施把类同时装入源 JVM 和它的目标 JVM 的实体模型的策略,就需要一个完整的类列表。如果当 Jalape�o 开始运行时,核心运行时环境的一个方法引用了不在引导映象中的任何类,则将产生无穷递归的结果:运行时环境要求装入它自己的一部分以装入它自己的一部分 ― 等等。
通过仔细的计划和反复试验,我们解决了判断实体模型中最少需要哪些类以阻止这种情形的问题。Jalape�o 的所有核心类都以 VM 为前缀命令。这里是提供使虚拟机能执行编译、内存管理和动态类装入的充足机器所需的类。这个专门的前缀由 Jalape�o 的编译器识别并用于抑制正常的动态链接规则:编译器从不在有这个前缀的类的方法之间生成动态链接代码。小心地编写核心类以避免对 Java 库类的不必要使用。这些基础类 ― java.lang.Object、java.lang.Class、java.lang.String 和一些 I/O 类 ― 是绝不可以排除在外的。VM_ 类和这些基础 Java 类构成了我们认为有必要在引导映象中出现的类的启动集。
然后通过反复试验识别出另外一小部分依赖(例如,Integer、Float、Double 和各种数组和异常类)。我们编译一个引导映象并试图执行它。如果它在试图(递归地)装入类 X时崩溃了,那我们就把 X 添加到写入引导映象的类的列表,并反复进行这个过程。这个过程集中进行了少数重试,也不证明一旦 VM_ 类的实现隐定了,这会成为一个维护问题。
实体模型在完成之后被转换成引导映象。这包括查找实体模型中的所有对象,把它们转换成 Jalape�o 的对象格式并存储进 引导映象数组。运行中的 Jalape�o 虚拟机的所有组件都可以从一个 JTOC 数组中获得(请参阅静态字段和方法部分)。在实体模型中,JTOC 被编码成三个并行的数组:一个整数数组(用于原始值),一个对象实例数组(用于引用)和一个用于区分这两个数组的布尔数组。JTOC 数组的结构被递归地遍历,所碰到的值(引用的和原始的)被转换到引导映象数组。由于每个装入类的类型信息块(请参阅对象头部分)都从 JTOC 引用,所以所有必需的编译后的方法体都将被包含到引用映象中。
这个转换过程使用了映象。引导映象为实体模型中的每个对象获得 java.lang.Class 对象并在由 getFields 方法返回的字段上反复进行一些操作。对每一个字段,它从源对象抽取字段值,从对象的 Jalape�o 类描述中抽取目标字段偏移量。然后,它把位于从对象的索引偏移该偏移量的值写入引导映象。当碰到对象引用时,我们不能使用来自实体模型的任何值。通过用一个作为分配到的引导映象维护的散列表,实体模型中的引用被转换成引导映象地址。(包含引导映象中的所有引用的地址的数组可以被包含进引导映象,以支持引导映象的重定位。)
总的来说,引导映象编写器一个字段一个字段地拷贝 Java 对象,从实体模型到引导映象,同时从源 JVM 的对象模型转换到目标 JVM 的对象模型。有赖于 Java 的映象功能,我们碰上了一个麻烦:Sun 的 Java 开发包(Java Development Kit),版本 1.1.4 不允许对私有字段的反射访问。这在 Java 2 软件开发包(Java 2 Software Development Kit)中不是一个问题,这个开发包允许这样的访问。通过预处理类文件,关掉私有位,我们在更早的版本中就解决了这个问题。
除了能从 JTOC 数组中访问的对象外,引导映象中还需要另外两个对象:一个初始线程对象和一个“引导记录”。初始线程对象包含一个空堆栈,它已为在 Jalape�o 启动时运行 boot( ) 方法的第一条指令准备就绪;“引导记录”是引导映象和引导映象运行器(稍后论述)之间的接口。这个引导记录包含映象中的开始、结束和最后使用的地址,也包含用于启动 Jalape�o 的四个寄存器值,boot( ) 方法的地址和 AIX 系统调用的地址。当这些值被存储到引导映象数组时,这个引导记录被写到磁盘上。
一个称为 引导映象运行器的短小程序启动 Jalape�o 的运行。它把引导映象读进内存,把四个寄存器设置为指定值并转到 boot( ) 方法分支。引导映象是用 C(带有一些汇编程序以设置寄存器和执行最后的分支)写的,不是 Java 代码,所以 它不需要在 JVM 上运行。
当 boot( ) 方法开始执行时,虚拟机处在一个脆弱状态:它能够运行机器指令的单个线程,但它还未创建支持它自己的执行所需的外部操作系统资源。引导映象无法创建这些操作系统资源,因为这些资源要引用外部状态,而这些状态将不存在,一直到引导映象执行。因此,Jalape�o 必须执行另外的初始化。
在引导期间,虚拟机初始化特定于硬件的地址(例如,它将最终在自己的堆栈上建立硬件监视页),打开与 Java 库的 System.in、System.out 和 System.error 流对象相对应的文件,分析命令行参数并创建与一个与当前执行环境相对应的 System.Properties 对象。然后,通过创建充当虚拟处理器的操作系统线程初始化多线程子系统,Java 线程在虚拟处理器上多路复用。最后,启用定时器中断以支持线程抢先,生成一个 Java 线程以运行命令行指定的应用程序。
Jalape�o 运行直到最后一个(非守护)Java 线程终止或调用了 System.exit( )。
B. Alpern has co-authored this article
C. R. Attanasio has co-authored this article
J. J. Barton has co-authored this article
M. G. Burke has co-authored this article
P. Cheng has co-authored this article
J.-D. Choi has co-authored this article
A. Cocchi has co-authored this article
S. J. Fink has co-authored this article
D. Grove has co-authored this article
M. Hind has co-authored this article
S. F. Hummel has co-authored this article
D. Lieber has co-authored this article
V. Litvinov has co-authored this article
M. F. Mergen has co-authored this article
T. Ngo has co-authored this article
J. R. Russell has co-authored this article
V. Sarkar has co-authored this article
M. J. Serrano has co-authored this article
J. C. Shepherd has co-authored this article
S. E. Smith has co-authored this article
V. C. Sreedhar has co-authored this article
H. Srinivasan has co-authored this article
J. Whaley has co-authored this article