java虚拟机的研究与实现


·56· 计算机与信息技术 经验与交流 Java 虚拟机的研究与实现 夏兵 俞建军 (南京航空航天大学 民航学院, 江苏省 南京市 210016) 摘 要 本文在研究 kaffe[1]的基础上,吸收 kaffe 虚拟机的主要思想,用 C 语言作为开发语言,采用了及时编译器作 为执行引擎,实现了一种 Windows 平台下的 Java 虚拟机。然后对实现过程中的一些关键技术如 class 文件验证、及时编译 器、垃圾收集器、线程同步和线程调度等做了分析。 关键词 kaffe; C 语言; 及时编译器;Java 虚拟机 0 引言 Java 虚拟机本质是就是一个程序,当它在命令行上启动 的时候,就开始执行保存在某字节码文件中的指令。Java 语 言的可移植性正是建立在 Java 虚拟机的基础上。任何平台只 要装有针对于该平台的 Java 虚拟机,字节码文件(.class) 就可以在该平台上运行。这就是“一次编译,多次运行”。 1 kaffe 虚拟机的简要分析 kaffe 虚拟机采用了模块化的程序设计思想,它由多个 独立的子系统组成。从功能模块上来分它主要分为:虚拟机 总体驱动模块, 类装载器模块,类执行模块, 数据区管理 模块,内存管理模块,本地支持模块等等。kaffe 虚拟机简 要的程序流程图如图 1 所示。 开始 结束 获取默认的 Java虚拟机参数 获取并处理用户设置的 相关的环境变量 检验被解析文件不带有.class扩展名 创建虚拟机 根据类名查找相关类,再将 该类装载 , 连接, 初始化 查找主类中的 main方法并且 解析main方法的字节码 图 1 kaffe 虚拟机简要的程序流程图 2 Java 虚拟机的实现 Java 源程序的执行过程为: Java 源程序(.java)经过 Java 编译器编译生成字节码文件(.class),然后由类装载器将字节 码文件装载到方法区中,然后进行连接验证,由 Java 虚拟机读 取字节码,转换为特定平台的指令,并且在对应的 CPU 中执行。 本实现中采用的流程框架如下图所示: 类装载、连接、初始化 及时编译 生成本地机器Java虚拟机 垃圾收集 线程同步 Java源程序 Java编译器 Java class文件 线程调度 图 2 本实现的主要框架 2.1 类装载、连接及初始化 类文件包括:魔数(magic),次、主版本号,常量池,类 或接口访问修饰符,常量池索引(this_class 和 super_class),接口表,域表,方法表,类或接口的属性信 息。其中最复杂的内容是常量池,它类似于传统语言编译过 程中用到的符号表。 从原始的 class 文件到可以被 Java 虚拟机执行的内部数 据格式,需要经过装载、连接和初始化这 3 个阶段。 装载是将 class 文件通过类装载器装载到在逻辑上被称 为方法区的内存单元中的过程。 连接又分为三个步骤:验证,准备和解析。验证是对字 节码的验证,可根据具体情况来确定被装载的类是否符合 Java 虚拟机规范[2]中规定的 class 文件格式,并确保它不会 破坏 Java 虚拟机的完整性。包括(1)类装载过程中的验证; (2) 检查 class 文件内部的连贯性, 一旦发现 class 文件格 式存在一处错误,则抛出 VerifyError 异 常 或 ClassFormatError 异常。确保每个 final 类不含有子类, final 方法不能被覆盖,以及常量池中所有的域引用和方法 引用有有效的名字和类型描述符号;(3) 对字节码流使用一 经验与交流 计算机与信息技术 ·57· 个数据流分析器进行验证。准备步骤的任务是创建域表,并 设置域初值。解析步骤是将类中的常量池中的类、接口、字 段和方法的符号引用替换成直接引用,以达到更快地访问数 据的目的。 在初始化阶段,Java 虚拟机设计者需要将类变量赋予正 确的初始值。 class 文件经过上述三个阶段的处理,虚拟机就获得了 该类的所有信息并且表示成能够容易操作的内部数据格式, 从而为方法的运行作好了充分的准备。 2.2 及时编译器 任何 Java 虚拟机实现的核心都是它的执行引擎。在由软 件实现的虚拟机中,执行引擎主要有一次性解释字节码、及 时编译器、自适应优化编译器三种方式。本实现采用了及时 编译的方式,它的特点是第一次被执行的机器码会被编译成 本地机器码。及时编译器将引入的字节码翻译成本地机器码, 然后直接执行机器码指令而不是解释字节码。 机器码指令保 存在内存中,由于在运行过程中编译的结果不被保存, 所以 程序下一次运行时,字节码将再一次被翻译成机器码。 如果一装载完字节码文件中的 Java 方法后,就对其进行 编译,则有点处理不恰当,因为还不清楚是否需要执行该方 法。编译一个不需要执行的方法, 将带来不必要的空间和时 间上的损失。 因此虚拟机设计者需要采用一种优化方案,即 只有需要被执行的方法才能被 JIT 编译,这个问题可以参照 kaffe 虚拟机中的 trampoline 来解决。 JIT 实现步骤:(1)对字节码进行验证并且划分基本块; (2)产生四元式;(3)根据四元式生成本地机器码;(4)操作数 地址回填。 开始 加锁 其它线程是否 正在处理此方法 该方法是否为 本地方法 代码验证 生成四元式 遍历四元式并生成机器码 地址回填 释放锁 返回 Y N N Y 图 3 及时编译器的流程图 在字节码指令模拟操作的时候,按其语义动作生成指令 属性四元式序列,指令属性四元式的结构为: (目的操作数, 源操作数 1,源操作数 2,语义动作) [3],四元式数据结构如 下: typedef struct Sequence{ void (*func)(struct Sequence*); //语义动作 union{ jvalue value; struct _label_ *labconst; //标号类型操作数 Method *methconst; //方法地址操作数 struct slotData **smask; struct slotData *slot; //槽操作数 }u[3]; uint8 type; //Sequence类型 uint8 refered; //该四元式的引用 struct Sequence *next; //下一个四元式 }Sequence; 其中目的操作数为Sequence.u[0],源操作数1为 Sequence.u[1],源操作数2为Sequence.u[2]。 Sequence.func 则代表语义动作,它主要用于生成该Sequence语义的本地机器 码。 指令属性四元组建立后就进入代码生成阶段,属性四元 组在形式上已经非常接近本地机器指令,只需要遍历该属性 序列,执行相应的语义动作函数,即可生成机器指令。语义 动作函数的功能包括操作数寻址、寄存器分配、建立指令连 接以及本地机器码生成等。 在及时编译过程中要经常使用到操作数栈,虚拟机把操作 数栈作为它的工作区。大多数指令都要从这里弹出数据,执行 运算,然后把结果压回操作数栈。而操作数栈区,局部变量区 和帧数据区被包含在方法帧中。方法帧的数据结构如下: typedef struct Frame{ struct Frame *prev; // 上一帧 struct Frame *next; // 下一帧 value_t *sp; // 栈槽指针 uint8 *pc; // 程序计数器 method_t *method; //指向正在被执行的方法 class_t *class_ptr; // 指向包含该方法的类 value_t locals[1]; //方法的局部变量的起始 }Frame; 本实现中的及时编译器的优点表现在:(1)大大提高了 Java 应用程序运行的速度;(2)编译过程只在运行时进行,不 会改动 Java 字节码,不会影响 Java 程序的可移植性;(3)对 字节码的编译,使得许多优化手段的采用成为可能。缺点表 现在:(1)如果对所有方法进行编译,则会占用大量的内存空 间;(2)及时编译的结果在虚拟机终止运行时不被保存,这意 味着下一次运行同样的程序仍需要重复编译。 2.3 垃圾收集 垃圾收集器主要的任务是检测出垃圾对象,然后回收垃 圾对象使用的堆空间并还给程序。kaffe 采用了增量垃圾收 集的算法,而本实现中采用了三色标记并清除算法。 在标记之前先将堆中所有的分配单元置成白色,然后按 深度优先算法遍历每一个单元。 当垃圾收集器遍历一个分支 的时候,如果一个分配单元及与之相关联的单元都被遍历到, ·58· 计算机与信息技术 经验与交流 则将其标记成黑色。 如果一个单元被遍历到,但是与之相关 联的单元尚未被遍历,则将该单元标记成灰色。这时,垃圾 收集器将继续遍历与该灰色单元相关联的单元,直到这些相 关联的单元全部被遍历到,才能将这个灰色单元标记成黑色。 最后当所有被遍历到的单元都被标记成黑色的时候, 将堆中 被标记成白色的分配单元回收。 图 4 三色标记并清除算法的中间过程图 最后是对堆碎块进行压缩处理。是通过快速地移动对象 来减少堆碎块。即把当前活动的对象移动到堆的一端,在此 过程中,堆的另外一端出现一块大的连续的内存单元。所有 被移动的对象的引用也被更新,指向新的内存单元。 2.4 线程同步 Java 虚拟机中存在着以下两种线程:虚拟机系统线程和 用户 Java 线程。虚拟机系统线程是指虚拟机运行过程中执行 其特殊功能的线程,比如垃圾收集器线程等。用户 Java 线程 是指用户编写的 Java 应用程序中明确表示要启动的线程,并 且至少有一个 Java 线程,即 main 方法。 而Java语言的一大优势是支持多线程,这种支持主要表 现在同步上。在java应用程序中使用synchronized关键字简 单地使方法同步,而在Java虚拟机指令中则使用 monitorenter和monitorexit指令显式地支持方法同步。Java 虚拟机为每个对象都关联一个锁。当当前线程访问共享资源 的时候,会执行monitorenter指令来弹出该对象引用,从而 获取该对象引用相关联的锁。如果该对象已经被另一线程占 用则当前线程就需要进入锁的等待队列,等待释放对象上的 锁;已经获取共享资源的线程在释放资源的时候,执行 monitorexit指令来弹出对象引用,并且释放与该资源相关联 的锁,并让等待队列中的第一个线程获取该对象锁。 当然线程 thread 也可以根据需要对某对象 obj 多次上 锁,上锁的次数放在计数器 counter 中。只有当 counter 为 0 的时候,即 thread 加在该对象上的锁被完全释放,其它线 程才有机会使用 object。对象的数据结构为: typedef struct Obj{ uint32 size; //堆中对象的大小 uint16 counter; //对象被上锁的数量 uint16 flag; //对象的状态标志 uint16 thread_id; //对该对象进行加锁的 //线程的ID } Obj; 而在实际Java编程中, 程序员并不需要动手加锁,对象 锁只是在Java虚拟机内部使用的。程序员只需要编写同步语 句就可以标志一监视区域,当Java虚拟机运行程序的时候, 每次进入一个监视区域,它每次都会给对象上锁。 2.5 线程调度 在本实现中,还需要考虑到在上述等待线程队列中如何 选择下一个线程来执行,即线程调度问题。 哪个线程将获取notify命令,这一点在很大程度上取决 于虚拟机的设计者,既可以通过使用FIFO队列来调度,也可 以根据所有等待线程的优先级来调度,比如唤醒等待队列中 优先级最高的线程获取刚刚释放的资源。而Bill Venners则 从平台无关和执行效率这两个角度出发,提倡Java虚拟机的 设计者应使用java.lang.Object类中的notifyAll()方法来 代替notify()方法去唤醒等待队列中的线程[4]。 处理好线程调度问题,就可以节省程序的执行时间,这 对于提高Java虚拟机的执行性能是很有帮助的。 3 总结 本文在研究kaffe的基础上,实现了一虚拟机。并且对 Java虚拟机中的关键技术及时编译器、垃圾收集器、线程同 步和线程调度等做了分析。本文中所实现的及时编译器虽然 在执行速度上比解释型的Java虚拟机快得多,但是不如自适 应优化编译器,因为自适应优化编译器具有程序启动快,占 用内存少的特点。如果要明显地提高虚拟机的性能,应该更 多的从执行引擎着手。另外在Java应用程序的执行过程中许 多时间是花费在多线程处理和垃圾收集上,如果在线程同步 和线程调度上有所创新,也可以提高虚拟机的执行性能。 研究Java虚拟机的实现过程有重要的意义,程序员可以 编写针对于不同平台下的裁减了的Java虚拟机,这样它就可 以在实时嵌入式系统得到广泛地应用。相信Java虚拟机将在 更多的领域得到不断的完善和发展。 参考文献 [1] kaffe.org. kaffe-1.1.7.zip[EB/OL] http://www.kaffe.org, 2006-3-27 [2] sun. Java虚拟机规范[EB/OL] http://pcbook.51soft.com, 2006-5-30 [3] 吕映芝 张素琴 蒋维杜. 编译原理[M] 北京:清华 大学出版社,2002. 161-162 [4] Bill Venners. 深入 Java 虚拟机[M] 北京:机械工 业出版社,2005. 346-347 收稿日期:7 月 31 日 修改日期:8 月 9 日 作者简介:夏兵(1982-), 男, 江苏盐城人, 主要研究方 向: 电子信息处理、实时嵌入式Java系统。 致谢:忠心感谢我的导师乔兵教授及师弟黄海泳,是他 们的热心帮助和善意地提出修改意见,使得我能顺利完成这 篇论文。
还剩2页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 3 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

crusader

贡献于2013-01-31

下载需要 3 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf