深入分析了 Kaffe虚拟机的 JIT(Just-In-Time)实现原理,以及在 JI中如何利用Trampoline技术来作为跳板达到提高 Kaffe虚拟机的执行性能,并通在 i386上结合实例来具体了解 Trampoline的实现。最后深入分析了作为 JIT核的翻译器在 JIT中如何将字节码映射成为中间码,并翻译成为本地代码的实现原理。
目录
1 引言
2 Kaffe中 JIT的实现原理
2.1 引入 Trampoline
2.2 Trampoline的实现思路
2.3 Trampoline的源码实现
2.4 翻译器的实现原理
3 结 束 语
1 引言
任何 Java虚拟机实现的核心都是它的执行引擎(如图 1)。Java虚拟机中的执行引擎就好比中央处理器,使得 Java虚拟机重复不断地读取字节码然后解释并执行,直到虚拟机进程退出。
Java虚拟机规范中规定,执行引擎的行为由指令集来规定动作。对于每条指令,规范都详细规定了执行该指令时应该“做什么”,但是没有说明“如何做”,因此 Java虚拟机的实现者可以采取解释执行技术、即时编译(JIT)技术或者直接在专用芯片上执行指令的技术,甚至可以是它们的混合技术。
Kaffe是按照 Java虚拟机规范实现的一种虚拟机。它是基于源代码开放的自由软件,在大部分平台上都能够成功的移植,且性能稳定。其执行引擎的实现(即“如何做”)也具有自身的特点。
Kaffe的执行引擎实现方式有解释执行与即时编译两种方式(可以在安装的时候选择)。解释执行是一种简单的实现方式:执行引擎读取每条字节码指令,然后将每条字节码解释成为本地代码,如此反复。这样的执行引擎实现方式比较简单,但是执行效率非常低下,因为解释工作是逐条地反复进行,导致程序中会有大量代码重复执行而浪费了许多时间。不过,Kaffe的即时编译器的执行效率得到了很大的提高:它是在第一次调用某个方法的时候,才将方法的字节码翻译成为本地代码,并在以后再次调用这个方法的时候,直接调用本地代码。由于是对整段代码的翻译,而且可以缓存本地代码,从而极大提高了虚拟机的运行速度。另外它还可以对整段代码进行本地优化,使解释字节码的效率得到大大提高,节约了大量的调用时间和空间。
2 Kaffe中 JIT的实现原理
2.1 引入 Trampoline
Kaffe采用JIT模式运行的时候,JIT会认为它正在执行的总是本地代码,因此需要在运行 Java方法之前将方法翻译成为本地代码。一种可能是在虚拟机装载 class文件的时候,将该类的所有方法都提前预编译为本地代码,这样在需要调用某个方法时,直接调用本地方法即可。但是这样的代价是装载一个类会耗费大量时间,而且经过提前翻译的方法不一定会得到运行,这样造成了时间和空间上的极大浪费,降低了性能。
2.2 Trampoline的实现思路
Kaffe的即时编译器(JIT)采用 Trampoline技术,其基本原理是:
1)创建 Trampoline阶段:每当虚拟机在装载类的时候,会为这个类的所有方法创建一个派遣表,该表中的每一项指向一个被称为 Trampoline的函数(见图 2)。该 Trampoline函数包含有足够的信息来通知一个叫做翻译器的函数来将该调用方法的字节码翻译成为本地代码。
2)调用方法阶段:每当虚拟机第一次调用某个方法的时候,调用者首先在一个派遣表中查找到该
方法,如果该方法还没有被翻译为本地代码,则该方法所指向的Trampoline函数会跳转到第一阶段存储的翻译器函数来负责将该方法的字节码翻译成本地代码。翻译结束之后,派遣表中的该方法被修改成指向翻译后的本地代码内存地址,并且将本地代码的地址返回给调用者。这样,以后再次调用该方法的时候,可以直接从派遣表中跳转到本地代码执行。
如图 2所示,调用路线直接从 a走向 b。Trampoline在其中起到了跳板的作用。
2.3 Trampoline的源码实现
Kaffe虚拟机由于是将字节码翻译成为本地代码,所以根据不同的平台,其实现原理虽然一致,但具体实现细节稍有不同。下面以 Kaffe在 i38平台下为例来分析一下 Trampoline的源代码。
在 Kaffe源代码目录的 config/i386/jit.h中,有一个 methodTrampoline结构体和 FILL_IN_TRAMPO-LINE的宏定义(见图 3)。methodTrampoline就是图2中 Trampoline的数据结构,它有 4个数据项,C语言中定义为 PACKED,表示 fixup项和 call项是紧挨着的,而不是 4字节对齐。FILL_IN_TRAMPOLINE宏的作用就是前面 2.2中描述的第一阶段。call=0xe8是 i386体系结构的汇编代码对应的 call指令。i386_do_fixup_trampoline是一个用汇编代码实现的函数,(int)t是 Trampoline的内存地址,汇编指令call占用 5个字节,所以要减去 5,最后得到的 fixup值是一个偏移量,该偏移量被汇编指令 call(0xe8)调用,方便以后跳转到 i386_do_fixup_trampoline中去执行。meth指向需要翻译成为本地代码的方法字节码。在 i386_do_fixup_trampoline中是作为参数传递给函数 soft_fixup_trampoline的(该函数调用了翻译器 Translate函数)。where指向派遣表中调用方法的位置。where因为和 meth在内存中是紧挨着的,所以最终它也传递给了函数 soft_fixup_trampo-line。
接下来看看 i386_do_fixup_trampoline的巧妙之处:popl%eax是将上面 meth的内存地址传递给%eax,之后压栈(push%eax),就可以将%eax(也就是meth的地址)作为参数传递给 soft_fixup_trampoline函数 了。soft_fixup_trampoline函数 调 用 翻 译 器(Translate方法)将字节码翻译成为本地代码,然后更新派遣表,使之指向本地代码。最后再跳转到本地代码并执行本地代码(见图 4)。
Kaffe针对其它平台也有类似的实现,虽然具体细节略有不同,但是其最终目的都是为了从 Trampo-line跳转到翻译器,把字节码翻译成为本地码。
2.4 翻译器的实现原理
Trampoline只是 Kaffe在 JIT中实现的跳板,而真正的将字节码翻译成本地代码的过程是由 Kaffe的翻译器来完成的。
Kaffe的 JIT在将字节码翻译成本地代码之前,会将字节码先翻译成对应的中间码,被称作 icode(intermediatecode)。Kaffe的中间码指令集的目的是为了在 Kaffe移植到一个新的体系架构过程中最大限度的获得代码重用,获得快速、高效的开发进度。
通过 Trampoline的巧妙设置后,此时翻译器并没有真正的执行,因为这只是在类装载时期完成的,还没有真正的调用 Java方法。而一旦第一次调用某个 Java方法时,JIT就会跳转到翻译器中来翻译字节码为中间码,再翻译为本地代码。翻译器在 JIT中起着核心的作用,主要完成三个步骤:
1)字节码的分析阶段:获得当前方法所需的栈信息(比如栈的大小等)、所有局部变量的有用信息等。
2)翻译阶段:首先将单个字节码指令映射到相应的中间码,然后通过中间码生成被称为“se-quence”对象的链表,这些链表各自对应着跟体系结构相关的本地函数,最后通过这些本地函数将中间码翻译成本地代码。
3)连接阶段:将所有生成的本地代码拷贝到一个新的空间,并且初始化连接。这里的初始化连接表示重写某些因为拷贝到新的空间造成的地址改变等信息。
3 结 束 语
不同虚拟机的执行引擎都有自己的具体实现方式,这里分析了 Kaffe虚拟机在 JIT上的实现原理。通过在不同平台上的运行效果看,Kaffe的 JIT在执行性能上还是有其优势的。
随着 Java虚拟机在各种平台的应用越来越广泛,Java的跨平台性也得到了广泛的认可,从而做好Java虚拟机的移植工作是非常重要的。在移植中,Kaffe执行引擎的移植是重要的一环,在这方面,Kaffe已经做的很出色。通过本文的分析,能够为程序员理解以及移植 Java虚拟机的执行引擎带来一定的参考价值。