下面的几部分描述 Jalape�o 的对象模型(object model)、运行时子系统(run-time subsystem)、线程和同步子系统(thread and synchronization subsystem)、内存管理子系统(memory management subsystem)和编译器子系统(compiler subsystem)。
Java 对象的布局考虑了这几个因素:能实现对字段和数组元素的快速访问,能实现硬件空指针检查,能提供四指令虚方法调度,能进行执行频度较低的操作如同步、类型确切的垃圾回收以及散列。同时也支持对静态对象和方法的快速访问。
在传统的 JVM 中,运行时服务 ― 异常处理,动态类型检查,动态类装入,接口调用,输入和输出,反射(reflection)等等 ― 都是用写成 C、C++ 或汇编程序的 本机方法实现的。在 Jalape�o 中,这些服务主要用 Java 代码实现。
Jalape�o 在 虚拟处理器上对 Java 线程进行多路复用,不是把 Java 线程作为操作系统线程实现,而是在虚拟处理器上把 Java 线程作为 AIX pthread 实现。 4 Jalape�o 的锁定机制的实现无须操作系统支持。
Jalape�o 支持一系列内存管理器,每个内存管理器由一个对象分配程序和一个垃圾回收器组成。所有分配程序是并发的。当前,所有回收器都是阻止一切的、并行的和类型确切的回收器。繁衍的和不繁衍的,拷贝的和非拷贝的管理器都被支持。增量回收器正在研究之中。
Jalapeno 不解释字节码。而是在执行之前把这些字节码编译成机器代码。Jalape�o 支持三个可互操作的编译器,它们处理开发期间、编译期间和运行期间的之间的不同权衡问题。这些编译器是 Jalape�o 设计整体的组成部分:它们支持了线程调度、同步、类型确切的垃圾回收、异常处理以及动态类装入。
对象模型和内存布局 Java 语言中的值或者是 基本类型(例如,int,double 等)或者是对象 引用(即指针)。对象或者是包含元素的 数组或者是包含字段的 标量。Jalape�o 的对象模型依从四条标准:
- 字段和数组访问应是快速的。
- 虚方法调度应是快速的。
- 空指针检查应由硬件执行。
- 其它(执行频度较低的)Java 操作不应慢得造成抑制。
假设对象引用保存在一个寄存器中,那么就可以只用一条指令以固定的偏移访问对象的字段。为简化对数组的访问,我们让对数组的引用指向数组的第 1 个(下标为 0)元素,余下的元素按升序分布。数组元素的数量,即数组的 长度,就保存在第 1 个元素前面。
Java 语言要求,如果试图通过空对象引用访问对象,那么将产生 NullPointerException。在 Jalape�o 中,引用是机器地址且用“地址 0”表示空(null)。AIX 操作系统允许从低端内存装入东西,但当访问超高端内存(从空指针 负偏移一点)时,通常会导致硬中断。 5 这样,对偏离空 Jalape�o 数组引用地址的访问的尝试将被硬件捕获,因为数组访问要求装入数组长度,数组长度偏离数组引用 4 个字节。将字段定位在对象引用的负偏移位,使对字段访问的硬件空指针检查受到影响。
总之,在 Jalape�o 中,数组从对象引用(数组长度位于固定的负偏移位置)处 向上生长,而标量对象则从对象引用处 向下生长,标量对象的所有字段都位于负偏移位置(请参阅图 1)。只用一条指令以基址-偏移(base-displacement)方式寻址即可完成对字段的访问。大多数数组访问需要三条指令。一条陷阱指令即可验证索引是在数组的边界内。除了字节型(和布尔型)数组,其它数组的元素索引然后都必须转成字节型索引。访问本身是用基址-索引(base-index)寻址方式完成的。
对象头(Object header)。每个对象都有一个两个字长的对象头与之相联系。这个头支持了虚方法调度、动态类型检查、内存管理、同步以及散列。它的位置在比对象引用的值低 12 字节的地方。(这为对象是一个数组的情况留了一个存放数组长度字段的空间,请参看 图 1。)
头中的一个字是 状态字。状态字分成三个 位字段。第一个位字段用于锁定(稍后论述)。第二个位字段保存散列对象的缺省散列值。第三个位字段由内存管理子系统使用。(这些位字段的大小取决于编译时常数。)
对象头的另一个字是本对象的类的 类型信息块(Type Information Block(TIB))的引用。TIB 是由 Java 对象引用组成的数组。它的第一个元素描述对象的类(包括类的超类,类实现的接口,任何对象引用字段的偏移量等等)。余下的元素是类的虚方法(Virtual method)的编译后的方法体(可执行代码)。这样,TIB 就作为 Jalape�o 的虚方法表(virtual method table)。
虚方法。方法体是机器指令(int s)的数组。一个虚方法调度牵涉装入 TIB 指针(位于与对象引用有一个固定偏移量的地方),装入方法体(位于从 TIB 指针偏移给定偏移量的地方)的地址,把这个地址移入 PowerPC “链接寄存器(link-register)”,以及执行一条转移和链接(branch-and-link)指令 ― 共四条指令。
静态字段和方法(及其它)。所有静态字段和所有静态方法体的引用都存储在一个名为 Jalape�o 内容表(Jalape�o table of Contents,JTOC)的数组中。这个数组的引用被保存在一个专用机器寄存器(JTOC 寄存器)中。Jalape�o 的所有全局数据结构都可以通过 JTOC 访问到。文本、数值常数和字符串常数的引用也都存储在 JTOC。为了能够快速进行普通情况动态类型检查,JTOC 也为系统中的每一个类保存其 TIB 引用。图 2 描绘了 JTOC。尽管 JTOC 被声明为 int s 数组,但它包含所有类型的值。一个与 JTOC 一起被索引的 JTOC 描述符数组标识了包含引用的条目。
方法调用堆栈(Method invocation stack)。图 3 描绘了 Jalape�o 的堆栈布局。每个方法调用都有一个堆栈帧。(优化编译器将删除叶方法和内联方法的堆栈帧。)堆栈帧包含用于保存非易失性寄存器的空间,一个用途依赖于编译器的本地数据区,还有一个用于保存参数的区域,这些参数将被传递给被调用方法,而且不适合被放在 Jalape�o 的易失性寄存器中。堆栈帧的最后三个字是:一个 编译后的方法的标识(堆栈帧的方法的标识信息),一个 指向下一条指令的指针(任何 被调用方法的返回地址)和一个 指向前一帧的指针。
方法调用堆栈和 JTOC 是仅有的两个违反 Java 语言规定的 Jalape�o 结构,Java 语言规定数组不能同时包含基本类型和引用。因为方法调用堆栈和 JTOC 都不能被用户直接访问,所以这并不是一个安全性疏忽。然而,为了使类型确切的垃圾回收更容易,Jalape�o 的编译器必须维护引用映射表(reference map)(稍后论述)以使我们能够找到堆栈中的对象引用的位置。
运行时子系统 通过巧妙使用 MAGIC 类(请参阅 附录 A),Jalape�o 的运行时子系统用 Java 代码(多数)提供服务 ― 异常处理、动态类型检查、动态类装入、接口调用、I/O、反射等等 ― 这些服务惯常是用本机代码实现的。
异常。如果空指针被解除引用、数组索引越界、整数被零除或者线程的方法调用堆栈溢出,那么就将产生硬中断。一个小型的 C 中断处理程序将捕获这些中断并使一个 Java 方法运行。这个方法构造适当的异常并将异常传给 deliverException 方法。软件生成的异常也调用 deliverException 方法。这个方法有两项任务。首先,它必须保存异常对象信息,以便在有需要时能打印堆栈跟踪。它通过向上"遍历"堆栈并记录每个堆栈帧的编译后的方法的标识和指向下一条指令的指针来完成这项任务。其次,它必须把控制转到适当的“catch”块。这也涉及遍历堆栈的步骤。对每一个堆栈帧,它定位方法体(这个方法体生成堆栈帧)的编译后的方法对象。它调用这个对象的一个方法来判断异常是否恰巧在适当的“try”块。如果是这样,控制就被转到相应的 “catch” 块。如果不是这样,那么释放该堆栈帧占用的任何锁,而且也释放该堆栈帧。如果未找到 catch 块,该线程就被杀死。
动态类装入。Java 语言的一个创新特色是在应用程序的执行期间装入类。当 Jalape�o 编译器碰到一个字节码(例如 putstatic 或 invokevirtual)― 该字节码引用了尚未被装入的类 ― 时,编译器不是立即装入该类。相反地,编译器生成那些在第一次 被执行时确保了所引用的类已经被装入(解析并实例化)的代码,然后完成操作。注意,当这些代码被生成时,编译器无法知道该类的字段(例如,指定给一个静态字段的 JTOC 索引)和方法的实际偏移量(因为它们未被指定,直到该类被装入)。基线编译器(baseline compiler)(Jalape�o 的编译器都稍后论述)通过生成一些代码来处理这种不确定情况,这些代码调用运行时例程,运行时例程将执行任何必要的类装入操作,然后用基线编译器已经生成(如果已在最初代码生成期间解析了类的话)的代码来覆盖调用点。在处理器与松散内存聚合模型相连的 SMP 上,这将特别棘手。 6
优化编译器采用另一种方案,这种方案基于额外添加的一级间接;当编译器被执行时,编译器生成的代码从偏移数组中装入一个值。如果偏移值是有效的(非零值),它就被用来完成操作。如果偏移值无效,就装入所要求的类,在此过程中,偏移数组被更新以包含类的所有方法和字段的有效值。对每一个动态链接点,基线编译器的方案使得第一次执行该点时,开销非常大,但随后的执行则不产生开销,而优化编译器的方案在每次执行该点时,会产生很小的开销。然而,优化编译器通常不会被一个方法调用,直到该方法被执行多次;对任何动态链接点,这个编译器看来可以假设极少被执行。哪种办法最适合于快速编译器(quick compiler)仍不清楚。
输入和输出。I/O 需要操作系统支持。要从一个文件读一个块,须构造一个 AIX 堆栈帧并用一个地址调用(通过 C 库)一个操作系统例程,该地址用于拷贝它的结果。这个地址是一个 Java 数组。要小心防止垃圾回收器副本移走对象,直到调用完成(请参阅 附录 A 了解详细信息)。到目前为止,我们尚未观察到因把垃圾回收一直延迟到读操作完成带来的性能降低问题。其它 I/O 服务的处理相似。
反射。Java 的反射机制允许对字段的运行时访问(给定字段的名称和类型)和对方法的运行时调用(给定方法的说明)。Jalape�o 很容易就可支持反射字段访问:名称被转换成偏移量,在裸内存地址执行访问。反射方法调用要困难一点。通过在一个表中查找方法的说明获得方法体的地址。构造了一个人工堆栈帧。(因为这个堆栈帧不包含任何对象引用,所以没必要为它构建一个引用映射表。)方法的参数被小心地分解并装入寄存器。于是就调用了一个方法。当方法返回时,必须清除人工堆栈帧,而把结果包起来并返回给反射调用。
线程和同步子系统。 不是把 Java 线程直接映射到操作系统线程,Jalape�o 在 虚拟处理器上多路复用 Java 线程,虚拟处理器是作为 AIX pthread 实现的。这样做是基于三个考虑。我们要能够实现变形(通过正常线程)和垃圾回收之间的快速切换。我们想不使用 AIX 服务而实现锁定。我们想支持快速线程切换。
目前,Jalape�o 为每一个物理处理器建立一个虚拟处理器。另外的虚拟处理器最终将用于屏蔽 I/O 延迟。这个子系统需要的唯一一项 AIX 服务是 incinterval 系统调用提供的周期定时器中断。Jalape�o 的锁定机制不产生系统调用。
准强占式(Quasi-preemption)。Jalape�o 的线程既不是“运行直至被阻塞(run-until-blocked)”的也不是完全强占式的。依赖自动让出将使得 Jalape�o 不能保证满足服务器环境发展的需要。我们觉得任意抢先会极大地将垃圾回收和线程堆栈上的对象引用标识之间的切换复杂化。在 Jalape�o 中,一个线程可以被抢先,但只能在预定义的 让出点(yield point)被抢先。
编译器在让出点为线程堆栈上的对象引用提供位置信息。堆栈上的每个方法都在一个 安全点(safe point)(稍后论述)。这使得编译器能够在安全点之间优化代码(例如,通过维护一个内部指针),如果允许任意抢先,这些安全点将阻止类型确切的垃圾回收。
锁。SMP 上的并发执行要求同步。线程调度和负载平衡(尤其是)要求对全局数据结构进行原子访问。用户线程访问它们的全局数据也需要同步。为同时支持系统和用户同步,Jalape�o 提供了三种锁。
处理器(processor)锁是低级原语,用于线程调度(和负载平衡),也用于实现 Jalape�o 的其它锁定机制。处理器锁是 Java 对象,它只有一个字段,用于标识拥有这个锁的虚拟处理器。如果该字段为空,则这个锁就未被拥有。处理器的标识保存在一个专用的 处理器(PR)寄存器。虚拟处理器要为它运行的线程获取一个处理器锁,就将:
- 装入设置一个 CPU 保留(PowerPC lwarx)的锁所有者字段
- 验证该字段为空
- 在一定条件下把 PR 寄存器存储到该字段(PowerPC stwcx)
如果 stwcx 成功,虚拟处理器就拥有了该锁。如果所有者字段不空(另一个虚拟处理器拥有了该锁),或者 stwcx 指令失败,则虚拟处理器将重试(也就是说,自旋(spin))。通过把空值(null)存储到所有者字段来实现一个处理器锁的解锁。处理器锁不能被递归地获取。因为处理器锁“忙等待(busy wait)”,所以对它的占用时间间隔必须极短。当一个线程拥有一个处理器锁时,它不能被切换,有两个原因:因为在继续执行前,它不能释放该锁;因为我们的实现将不适当地把锁的所有权转给在虚拟处理器上执行的其它线程。
Jalape�o 的其它锁定机制的基础是 瘦锁(thin lock): 7, 8 不存在争用时,对象头的位用于锁定;存在争用时,这些位则标识一个重量级锁(heavyweight lock)。Jalape�o 的做法有两点与以前不同。在以前的做法中,重量能锁定机制是一个操作系统服务;这里则是一个 Java 对象。这里,如果“线程 A”有一个某对象的瘦锁,则“线程 B”可以把这个锁提升为 厚锁(thick lock)。在以前的做法中,只有拥有瘦锁的线程才能提升它。
对象头(请参阅前面的讨论)的状态字中的一个位字段用于锁定。这个位字段的一个位标明是否有一个厚锁与对象相联系。如果是这样,则这个位字段的其余位就是这个锁在全局数组中的索引。这个数组被分成若干个虚拟处理器区以允许对厚锁的非同步分配。如果该厚锁位未被设置,则位字段的其余部分被分成两部分: 瘦锁所有者(thin-lock owner)子字段标识了占用该对象的瘦锁的线程(如果有的话)。(位字段的大小可以调整,支持的线程数可高达 50 万。) 递归计数(recursion count)子字段保存锁所有者占用锁的次数:不象处理器锁,瘦锁可以被递归地获取。如果一个对象未被锁定,则整个锁定位字段为零。
为获取一个瘦锁,线程把瘦锁所有者位字段设置为自己的标识。只当锁定位字段为零时才允许这样做。当前正在一个虚拟处理器上运行的线程的标识保存在一个专用的 线程标识(thread identifier(TI))寄存器。lwarx 和 stwcx 指令再次被用来确保自动获取瘦锁。
厚锁是一个有六个字段的 Java 对象。mutex 字段是一个处理器锁,用于同步对厚锁的访问。associatedObject 是一个对象引用,该对象是厚锁当前管理下的对象。ownerId 字段包含拥有该厚锁的线程(如果有的话)的标识。recursionCount 字段记录所有者锁定该锁的次数。enteringQueue 字段是正在争用该锁的线程队列。而 waitingQueue 字段则是正在等待 associatedObject 的通知的线程队列。
要把一个瘦锁转换成一个厚锁,必须:
- 创建一个厚锁
- 获取它的 mutex
- 装入该对象的状态字以设置一个保留(lwarx)
- 在一定条件下把适当的值 ― 厚锁位设置和这个厚锁的索引 ― 存储(stwcx)进对象头的锁定位字段
- 重复步骤 3 和 4 直到有条件的存储成功
- 填充该厚锁的字段以反映对象的状态
- 释放厚锁的 mutex
关于 PowerPC 上的锁定有两点要详细考虑。第一,(lwarx 的)保留除因争用外还有各种原因可能会被丢失,包括与该保留的字相同的高速缓存行的存储或者虚拟处理器的操作系统上下文切换。第二,在释放一个锁(任何类型)之前,必须执行一个同步以确保其它 CPU 上的高速缓存看到在锁被占用期间所做的任何变化。类似地,在获取一个锁之后,也必须执行一条 isync 指令以使随后的指令不会在一个过时的上下文中执行。
线程调度(thread scheduling)。Jalape�o 实现了一个瘦线程调度(lean thread scheduling)算法,设计该算法是为了有一个短的路径长度,把同步降到最少并为负载平衡提供一些支持。线程切换(和锁)用于实现准抢先,也用于实现 java.lang.Object 的 yield 和 sleep 方法以及 java.lang.Thread 的 wait、notify 和 notifyAll 方法。一个线程切换由以下操作组成:
- 保存老线程的状态
- 从等待执行的线程队列中删除一个新线程
- 把老线程放到某个线程队列(如有必要,锁定该队列)
- 释放监视对这个队列的访问的进程锁(如果有的话)
- 恢复新线程的状态并恢复它的执行
在与虚拟处理器相联系的处理器对象中有三个可执行线程的队列。idleQueue 保存一旦没有别的事可做时就将执行的空闲线程。readyQueue 保存其它执行就绪(ready-to-execute)的线程。只有与它们相联系的虚拟处理器才能够访问这两个队列,因此,更新时不需要锁定它们。这个虚拟处理器是唯一能从 transferQueue 中把线程除去的虚拟处理器。然而,其它虚拟处理器可以把线程放入这个队列,所以对它的访问要用一个处理器锁来同步。transferQueue 用于负载平衡的目的。
管程(monitor)。Java 语言支持 管程抽象 9 以允许用户级同步。从概念上说,每一个 Java 对象都有一个管程与它相联系。然而,极少有管程曾被用到过。典型情况下,线程通过执行对象的同步方法来获取对象的管程。一次只能占用少数管程。一个线程可以(递归地)多次获取同一个管程,但没有线程能够获取另一个线程占用的管程。Jalape�o 用它的锁定机制来实现这种功能。
当一个线程试图获取一个对象的管程时,有六种情况,这取决于谁拥有该对象的管程以及该对象是否有一个与之相联系的厚锁:
- 对象未被拥有 ― 没有厚锁。就象前面所说的一样,线程获取对象的瘦锁。(这是到目前为止最普遍的情况。)
- 对象被这个线程拥有 ― 没有厚锁。线程用 lwarx 和 stwcx 指令把状态字的递归计数位字段加 1。这个同步是必须的,因为另一个虚拟处理器可能正同时把该瘦锁转换为厚锁。如果这个位字段溢出,那么这个瘦锁就被转换成厚锁。
- 对象被另一个线程拥有 ― 没有厚锁。这是一种有趣的情况。有三种选择。线程可以:重试(忙等待),让出并重试(给另一个线程一次执行的机会)或者把瘦锁转换成厚锁(情况 6)。我们正在研究这三种选择的各种组合。
- 对象未被拥有 ― 有厚锁。我们获取厚锁的互斥锁(mutex),验证该锁仍与对象联系在一起,把线程索引(thread index(TI))寄存器存储到 ownerId 字段,然后释放互斥锁。到互斥锁被获取时,可能厚锁已经被解锁,甚至已经解除和对象的联系。在这种极其少见的情况中,线程开始试图获取对象的管程。
- 对象被这个线程拥有 ― 有厚锁。我们杀死 recursionCount。由于只有拥有厚锁的线程才能够访问厚锁的 recursionCount 字段(或者释放该锁),所以同步是不需要的。
- 对象被另一个线程拥有 ― 有厚锁。我们获取互斥锁,验证该锁仍然与适当的对象联系在一起,并让给 enteringQueue,同时释放互斥锁。
我们正在研究两个与释放管程有关的问题:当一个厚锁被解锁时,要对 enteringQueue 中的线程做些什么;何时解除一个对象和一个厚锁的联系。
内存管理子系统 在 Java 语言的所有特征中,自动垃圾回收可能是最有用的,要高效实现它,也是最有挑战性的。有很多途径可以实现自动内存管理, 10, 11 但没有哪一条途径在服务器环境中具有明显的优越性。Jalape�o 被设计成支持一系列可互换的内存管理器。当前,每个管理器由一个并发对象分配器和一个阻止一切的、并行的、类型确切的垃圾回收器组成。得到支持的四个主要的管理器类型是:拷贝、非拷贝、繁衍拷贝和繁衍非拷贝。
并发对象分配。Jalape�o 的内存管理器把堆分区分为 大型对象空间和 小型对象空间。每个管理器使用一个非拷贝大型对象空间,这种空间被象管理一系列页面那样管理。用首次拟合法来满足对大型对象的请求。进行一次垃圾回收之后,邻近的空闲页面被合并。
为了通过拷贝管理器支持对小型对象的并发分配,每个虚拟处理器维护一大块本地空间,在这个空间上,对象不用请求同步就可获得分配。(这些本地大块空间不是逻辑上独立于全局堆的:由一个虚拟处理器分配的对象可被任何获取该对象引用的虚拟处理器访问。)分配通过把空间指针按所要求的大小增大并将增大后的结果与本地大块空间的边界比较来完成。如果比较失败(不是正常情况),则分配器自动从共享全局空间获得一个新的本地大块空间。这种技术不要求锁定,除非有人请求一个新的大块空间。维护本地大块空间的代价是内存碎片会轻微增加,因为每个块可能不会完全拟合。
非拷贝管理器把小型对象堆分成固定大小的块(当前是 16 千字节)。每个块再被动态地细分成固定大小的槽(slot)。这些槽的数量(当前是 12)和大小都是编译时常数,编译时常数可被调优以拟合所观察到的小型对象大小的分布。当分配器收到一个空间请求时,它判断能满足请求的最小的槽的大小,并获得该大小的当前块。为免除锁定开销,每个虚拟处理器维护每个槽的一个本地的当前块。如果当前块已满(不是正常情况),则虚拟处理器就查找下一个其空间对当前块可用的块。如果所有这样的块都是满的(更是少见),则虚拟处理器从共享池获得一个块并将新获得的块作为当前块。由于块的大小和槽大小的数量都相当小,所以为每个虚拟处理器拷贝当前块的空间影响是无足轻重的。
从变化到回收。每个虚拟处理器都有一个回收器线程与之相联系。Jalape�o 以两种方式之一运作:或者变化器(正常线程)运行而回收线程空闲,或者变化器空闲而回收线程运行。当变化器显式地提出请求时,当变化器提出分配器不能满足的一个空间请求时,或者当可用内存的数量低于预设的阈值时,垃圾回收就被触发。
可伸缩性要求方式间的切换尽可能地敏捷。在变化期间,所有的回收器线程都处于等待状态。当一个回收被请求时,回收器线程接到通知并被调度(通常是作为下一个要执行的线程)到它的虚拟处理器。当一个回收器线程开始执行时,它将禁用它的虚拟处理器上的线程切换,让其它回收器线程知道它已控制了虚拟处理器,执行一些初始化并与其它回收器同步(在第一个集中点(rendezvous point),稍后论述)。当每个回收器都知道其它的正执行时,切换就完成了。
注意,当所有回收器线程在运行时,所有的变化器都必须位于让出点。没有必要重新调度以使任何先前暂挂的变化器线程到达这一点。当变化器线程的数量很大时,这可能会对性能有重大影响。由于 Jalape�o 中的所有让出点都是安全点,所以回收器线程现在可以继续进行回收工作。
完成回收之后,回收器线程重新启用它的虚拟处理器上的线程切换,然后等待下一个回收。当回收器线程释放它的虚拟处理器时,变化器线程自动启动。
并行垃圾回收。Jalape�o 的垃圾回收器被设计成以并行方式执行。回收器线程于三个阶段的每个结束点在回收器线程之间同步。为此,Jalape�o 提供了一个集中机制,在集中点,没有线程可以继续向前执行,直到所有的线程都到达集中点。
在 初始化阶段,拷贝回收器线程拷贝它自己的线程对象和虚拟处理器对象。这确保了对这些对象的更新作用在新的副本上,而不是老的副本,后者在回收之后将被废弃。
非拷贝管理器让每一块内存与一个标记(mark)数组和一个分配(allocation)数组联系在一起,每个数组都有一个到每个槽的条目。在初始化期间,所有标记数组的条目都被置零。所有回收器线程都参与这个初始化。
在 根标识(root identification)和扫描阶段,所有回收器的行为都相似。回收器线程争用 JTOC 和每个变化器线程堆栈,并行地扫描它们以获得根(就是说,从概念上说,对象引用在堆外),这些根被标记并被放到一个工作(work)队列。然后标记可从工作队列访问的对象。标记操作被同步,所以一个回收器正好标记一个活动对象。作为标记一个对象的一部分,拷贝回收器将把它的位拷贝到新空间并用指向该新拷贝的 转发指针覆盖老副本的状态字。(这个指针的低位中的一位被设置以表明该对象已被转发。)
JTOC 中的根通过检查联合检索描述符数组来标识,这个数组标识了每个条目的类型。线程堆栈中的根通过分析与每个堆栈帧相联系的方法来标识。特别地,本地数据区将拥有堆栈帧的任何普通根;参数溢出区将拥有下一个(被调用)方法的堆栈帧的根;非易失性寄存器保存区可能包含来自一些早期堆栈帧的根。根的定位是这样实现的:检查相应于堆栈上的方法的编译器构建的引用映射图并跟踪哪个堆栈帧保存了哪个非易失性寄存器。
全局工作队列在虚拟处理器本地块中实现,以避免过多的同步。从工作队列除去的对象被每个对象引用 扫描。(这些引用的偏移量是从类对象中获得的,该类对象是对象的 TIB 的第一个条目。)对每一个这样的引用,回收器都试图标记该对象。如果成功,回收器就把该对象添加到工作队列。在拷贝回收器中,标记操作(不管是成功还是失败)返回引用对象的新地址。
在 完成阶段,拷贝回收器只是反向设置堆的占用和可用部分。回收器线程从当前空闲的“托儿所”获得本地大块空间以为下一个变化器循环作准备。非拷贝回收器线程执行以下步骤:
- 如果这是繁衍回收器的一个小型回收,那么就把所有老对象标记成活动的(从当前分配数组中识别)。
- 扫描所有标记数组以找出空闲块并将它返回给空闲块表(list)。
- 对于所有块都不空闲的情况,交换标记和分配数组:老标记数组中的未标记条目标识了可用于分配的槽。
性能问题。我们正积极研究非拷贝和拷贝内存管理器,为的是更充分地理解哪种情况下谁是首选,并为研究混合解决方案提供可能。(非拷贝大型对象空间即是混合解决方案的一个示例。)拷贝内存管理器的主要优点是对象分配的速度快和在回收期间执行了堆的压缩操作(有更好的高速缓存性能)。非拷贝内存管理器的主要优点是回收更快(对象未被拷贝)、更好地使用了可用空间(拷贝管理器浪费了其中一半)以及变化器和回收器之间的交互作用更简单。(如果不必考虑对象可能会在每个安全点移动,那么优化编译器将会执行更强烈的优化。)使用拷贝管理器的系统,其回收之间的运行将整体上更快;使用非拷贝管理器的系统的暂停次数将更少。
非拷贝策略将极大地简化 并发内存管理器(其中的变化器和回收器同时运行):它不需要读障碍(read barrier)并简化了写障碍(write barrier)。
编译器子系统 Jalape�o 通过在运行时把字节码编译为机器指令来执行它们。三个不同但兼容的编译器已经投入使用或正在开发之中。Jalape�o 的开发基于一个早期的显然正确的可用的编译器上进行。这是 Jalape�o 的 基线编译器的角色。然而,顺便说一下,它不生成高性能的目标代码。
为获得被认为是计算密集的方法的优质的机器代码,Jalape�o 的 优化编译器(下一部分讲述)应用了大量新的、特定于动态 Java 上下文的优化措施,同时也应用了传统的静态编译器的优化措施。若将优化编译器用在只是偶而执行的方法上,则运行优化编译器的代价与得益相比是太高了。
Jalape�o 的 快速编译器将在每个方法第一次执行时对方法进行编译。它通过应用一些高效的优化措施来平衡编译时间和运行时间开销。寄存器分配是这些措施中最重要的,因为 PowerPC 有大量(32 个定点,32 个浮点)寄存器资源,而且多数寄存器操作是一个周期的,而存储器引用可能需要几个(有时很多个)周期。
快速编译器通过全面使用尽量少的转换、有效的数据结构以及尽量少传递源和派生数据的办法来尽量限制编译时间。源字节码不被转换成中间表示。相反地,字节码用分析和优化的结果“装饰”后被放入到与每一条字节码指令相关的对象。优化操作包括副本传播以清除由 Java 字节码基于堆栈的天然特性引入的临时文件。快速编译器的主寄存器分配器使用图染色算法。 12 染色对有些方法(例如,需要很多符号寄存器的 long one-basic-block static 初始化器)是不合适的(由于编译时间长)。对于这样的方法,快速编译器有一个更简单、更快的算法。我们将研究检测这些情况的试探法。我们也计划添加最后(final)、静态(static)的短(short)方法或构造器的内联编译并探索本地上下文(孔颈(peephole))优化。
由所有三个编译器生成的代码必须符合 Jalape�o 的调用和抢先约定。它们确保线程执行它所编译的方法时将会实时地对抢先这些线程的尝试作出响应。当前,显式让出点被编译成方法前言(prologue)。最后,循环的“后边缘”将需要让出点,循环不可包含其它让出点。
编译器也负责维护用于支持异常处理和使内存管理器能找到线程堆栈上的引用的表(table)。(Jalape�o 的调试器也使用这个表。)当一个垃圾回收事件发生时,线程堆栈代表的每个方法都将位于垃圾回收的 安全点。安全点是调用点、动态链接点、线程让出点、异常抛出的可能点和分配请求点。对方法体内的任何给定的安全点,创建该方法体的编译器必须能描述活动引用存在于哪里。 引用映射表为每个安全点标识了对象引用的位置。