赵睿
摘要:在符合Java虚拟机体系结构规范的前提下,JVM的具体实现可以有多种形式。论文在阐述了JVM体系结构规范后,为JVM各子系统提供了具体的软硬件实现方法,构造了一个完整的JVM实例,之后通过一个典型的Java程序示例,来验证在嵌入式系统开发中,软硬件结合方式构造JVM的可行性。
关键词:JVM;软硬件结合;嵌入式系统
中图分类号:TP311 文献标识码:A 文章编号:1009-3044(2012)34-8326-03
JVM(JavaVirtualMachine,Java虚拟机)用于运行Java程序,任何一个Java程序都是运行在符合规范的JVM上的。由于“规范”仅仅是描述性的、抽象的,因此可以通过不同的机制来实现JVM。它或者完全用软件实现,或者以软件和硬件相结合的方式来实现。传统的运行于操作系统上的JVM用软件方式来完成取指、分析、执行,必定会造成程序执行速度低下。在嵌入式系统中,软件执行方式更会因为嵌入式系统计算能力的限制而造成字节码解释执行速度过慢,从而影响性能。该文拟寻求一种软硬件结合的方式来构建JVM,在不改变用户开发习惯的基础上,寻求执行效率和硬件资源消耗的最佳化,以满足嵌入式系统开发需求。
1规范:JVM的体系结构
JVM的结构框图如图1,包括在规范中描述的主要子系统和内存区。它们的分工和作用如图1所述。
类装载子系统负责根据给定的全限定名来装入类型(类或接口)。
当JVM运行一个程序时,它需要内存来存储许多数据,这些数据被组织在“运行时数据区”中,以便于管理。其中,方法区包含了从class文件中解析的类型信息,如字节码、常量池、静态变量等,是执行的信息源头;堆用来保存程序运行时创建的所有类的实例(对象)或数组,并且,它是被所有线程所共享的,是开发人员可见的编程对象;每个Java栈帧包括局部变量区、操作数栈和帧数据区,分别用于保存传递参数及运算中间结果、提供当前运算的操作数、保存方法返回及异常派发机制的数据,是程序运行的直接数据交互区;PC寄存器是方法区中指令的指针,引导指令有序执行;本地方法栈用于同本地方法进行数据交互。
执行引擎的作用是从运行时数据区提取字节码和数据进行解析和执行,然后把中间结果或成员变量等数据写回运行时数据区,是JVM的执行核心。一个完整的执行引擎须支持全部的Java指令集。
本地方法接口为设计者实现低层的本地方法提供途径,以便和所使用的特定JVM结构更加紧密的结合。本地方法接口和本地方法栈不是必需的,具体实现可以不提供支持。
2实现:一个具体的JVM实例
如前所述,JVM的规范并没有针对具体的实现进行约束,这就为不同的JVM实现途径提供了可能。在满足JVM体系结构规范的前提下,各子部件可以寻求不同的软硬件实现方式。
2.1软件实现模块
预处理器是装载子系统的软件实现,它的输入是一个Java程序所包含的所有类文件(.class),输出是可以供硬件Java处理器直接执行的字节码流。事实上,除了实现类装载的任务外,预处理器还实现了执行引擎的一部分功能,即对类文件进行解析。
预处理器实现的解析功能包括:提取程序涉及的每个类文件的字节码流;将字节码流中的符号引用(包括静态变量、实例变量、调用及返回方法等)解析为具体的内存地址;将复杂抽象字节码转化为简单可执行的字节码组合;将所有处理后的字节码流整合成一个可供硬件处理器直接执行的二进制文件。由此可知,预处理器的实现是与具体的内存组织形式和Java处理器实现方式密切相关的。
2.2硬件实现模块
2.2.1运行时数据区实现
“运行时数据区”是由具体的硬件存储单元实现的。其内存组织形式见图2。
方法区中的内容被分解成了几个区域。其中字节码被保存在一块单独的存储单元中,如图2[a]所示。当程序执行时,字节码指令是只读的,因此它可以用一块只读存储器来保存。方法内的指令顺序存储,指令执行时以main方法为主线顺序执行,方法的调用和返回通过跳转来实现。从常量池中获取操作数的情况并不常见,同时为了避免执行前需要向内存中预存数据的操作,常量池并没有被组织到内存中,而是把从常量池中获取数据的行为在预处理器中转化成了具体的带操作数入栈指令bipush或sipush(Java字节码的指令集及其行为描述参见文献[1])。
如图2[b][c]所示,静态变量区被组织在内存的最低地址。由于在预处理器中已经解析出了每个类的静态变量占用内存空间的大小,因此每个类的静态变量取的基地址是可以确定的并应用于每个方法的初始化中。堆空间的大小是全部对象的空间的累加值,因此并没有涉及到垃圾回收机制。当调用一个新方法时,静态变量指针和堆指针被赋值,以确保该方法执行时的“作用区”在正确的位置。
栈帧中的操作数栈并没有分配给内存。由于操作数栈是参与运算的操作数的最直接来源,它用一组寄存器来实现以提高系统性能。寄存器的数目应不小于程序执行时的操作数峰值数目,这样才不会发生溢出错误。局部变量区和帧数据区被分配在堆空间的邻接区域,如图2[a][b]所示。当前方法需要调用其他方法时,需要为新方法开辟新的局部变量区和帧数据区,被调用方法返回时,所开辟的空间被回收,指针重新回到调用前的状态。图3为某一程序的方法调用及返回示意图,被构造成树状结构。局部变量区的总大小为该树全部路径中权值(代表每个方法的局部变量空间大小)和的最大值。帧数据区的总大小为该树全部路径中权值(代表每个方法的帧数据空间大小)和的最大值。
2.2.2执行引擎的实现
执行引擎是一个基于硬件资源实现的Java字节码处理器。处理器的基本特性如下:
五级流水线结构:取码、缓存、译码、执行、写回。取码段负责更新PC寄存器的值,并以PC值作为地址从指令存储器中不断读取字节码写入缓存中。缓存段用来支持Java字节码的RISC执行机制,实现了对不定长的字节码的智能读取。译码段可以产生控制信号,用于控制不同指令的执行和写回。执行段负责完成指令对应的操作数的数据传递、算数运算、逻辑运算和读写内存等操作。写回段把运算结果写回到栈操作数寄存器、基地址寄存器等寄存器中。
流水线阻塞解决数据冒险:如上所述的流水线的五级中,执行段从寄存器中读取数据,而写回段将运算结果写入寄存器。这样,就有可能产生数据冒险现象。假如第N条指令将数据写回到寄存器,而第N+1条指令需要从寄存器中提取操作数,那么第N+1条指令就会取到未更新的寄存器值,从而产生错误。一个解决数据冒险的方法是在这两条指令之间添加nop指令用来暂停执行、写回段,即阻塞了流水线一个周期。之后,第N+1条指令就可以获取已经更新的寄存器值。处理器可以根据相邻两条指令的字节码信息智能判断是否发生数据冒险,从而决定是否需要阻塞流水线。
Java字节码RISC执行机制:不同的Java字节码所带操作数的个数不同,因此它具有CISC的特点。字节码不定长的特征给处理器取指令模块的设计带来困难,由于字节码及其操作数的顺序存储特点,一旦“多取”或“少取”,就会引发后续一系列的错误,包括漏取错取字节码,漏取错取数据,甚至将操作数当做字节码来执行的情况。因此,需要寻找一种方法,能够智能读取字节码数据的长度,保证字节码的有序执行。为了解决字节码不定长的问题,引入“缓存”,它实质上是n(n>>1)个8位寄存器的组合。缓存在未满的情况下一直接收从字节码存储器读取的字节码及操作数并依次存储。同时,如果缓存存储了至少足够执行一次的字节码和操作数,字节码和其操作数将一并被取出执行,否则,处理器将处于等待状态,直到有“完备”的字节码和操作数可用。当执行微程序时,处理器执行完ROM中的微指令才执行下一条字节码,与此同时,缓存一直处于接收状态。因此,在执行微程序的“间隙”,取字节码处于工作状态,或许此后不必为了执行一个三字节指令等待两个时钟周期,因为缓存中已经积累了足够多的字节码数据可取。
微程序实现方法调用及返回:Java有专门的字节码支持方法的调用和返回(如invokevirtual、invokespecial、ireturn、return)。这些指令的实现涉及到“保存现场”、“参数传递”、“创建被调用方法现场”、“结果返回”、“恢复现场”等一系列行为,很难或者需要消耗大量硬件资源才能在一个流水线周期内完成。基于已实现的简单指令,利用微程序来执行这类指令是一个不错的方法,因此需要在硬件处理器中添加ROM来保存微程序。
本地方法栈和本地方法接口是非必须的,该文描述JVM实现并未涉及。
3小结
基于软硬件结合方式构建的Java虚拟机,满足JVM系统规范的要求。各子系统分别给出了明确、具体的软硬件实现方案。开发人员不需要改变编程习惯,借助于常用的集成开发环境,即可将工程应用于嵌入式系统的开发。
参考文献:
[1]王立东,张凯.Java虚拟机分析[J].北京理工大学学报,2002(2).
[2]严东华,张凯.Java虚拟机及其移植[J].北京理工大学学报.2002(2).
[3]丁宇新,程虎.Java虚拟机中无用单元的精确回收[J].计算机学报,1999(11).
[4]探矽工作室.深入嵌入式Java虚拟机[M].北京:中国铁道出版社,2003.