摘要:OpenJDK的开源吸引了很多想弄明白Java虚拟机如何运行的开发人员。本文基于HotSpot虚拟机源码,分析了Java虚拟机的运行机制,并进一步深入研究了服务器端C2即时编译框架,指出了HotSpot虚拟机高效运行的原因,为下一步深入优化打好基础。
关键词: Java虚拟机;HotSpot虚拟机;服务器端编译器;结构描述文件
一、引言
Java虚拟机技术提供Java标准平台的基础设施,提供对快速开发、部署关键业务的桌面和企业应用程序的解决方案。Java无处不在,而Java虚拟机正是支撑Java运行的秘密武器,它是一个在硬件平台、操作系统之上的一个庞大复杂的软件,涉及的理论和技术非常广而宽。
HotSpot虚拟机是Sun/Oracle JDK和OpenJDK的默认Java虚拟机[1],是基于Java虚拟机规范[2]的一个高效虚拟机实现,也是全世界使用最广泛、最具影响力的Java虚拟机。很多程序员默认HotSpot虚拟机等同于Java虚拟机。
二、HotSpot虚拟机
(二)执行架构
HotSpot虚拟机的执行架构示意图如图1所示[4-5]。
HotSpot虚拟机采用解释器与编译器并存的架构,解释器和编译器是相辅相成地配合工作的。解释器和编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以省去编译的时间,立即执行;当程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。在某些特定情况下编译模式也能够通过“逆优化”(deoptimization)退回到解释模式下继续执行。
(二)解释编译交互
同时存在解释执行和编译执行,涉及解释执行和编译执行互相转化,图2说明了解释执行和编译执行互相转换的途径[6]。
Java虚拟机中如果某个方法被编译,则下次执行同样的方法时切换为编译执行。编译执行的入口有2种情况:方法编译和核心循环编译。方法编译在下次执行前要切换方法调用的入口,改写成i2c adapter的首地址,这个adapter完成从解释执行转为编译执行的功能;同时要完成把解释执行的参数拷贝到编译执行的参数区,解释执行时,参数区直接位于栈中,对于编译执行,参数一部分位于寄存器中,一部分位于栈中,所以,移植过程中需要考虑传递参数的这种情况。核心循环编译是由循环体出发的,但编译器依然会以整个方法作为编译对象,执行的是栈上替换OSR算法,即在运行过程中直接用编译执行的方法替换解释执行的方法,而不是下次调用该方法时再做替换,OSR关键的是要复制解释执行时产生的局部变量,同步锁等。
编译执行到解释执行的也有2个入口,最普通的入口是编译执行的方法调用解释执行的方法;另一个入口叫逆(deoptimization)操作。在编译执行的方法中进行方法调用时会查询方法的入口地址,如果是静态方法,直接解析方法入口地址,否则就将解析方法入口地址,这个函数会判断被调用的方法是否已经被编译过了,如果没有被编译过就进入编译转解释执行的入口。逆优化则可能是在编译执行方法时可能由于某种原因需要重新解释执行。
三、C2即时编译
服务器版编译器是一个专门面向服务器典型应用的充分优化过的先进自适应编译器,它支持和传统编译器如C++编译器类似的许多编译优化流程,以及一些传统编译器所不能做的自适应优化。
(一)编译框架
C2即时编译器通过目标处理器平台的结构描述文件和指令匹配规则,提升优化效率,其结构图如图3所示:
编译器首先分析字节码并生成中间表示Ideal图,所有优化和代码产生都是基于它;接着进行平台无关优化并生成平台相关的MachNode图;最后进行平台相关优化,包括指令选择、代码重排、寄存器分配、窥孔优化,直至输出目标机器代码。
在指令匹配选择阶段,基于确定有限状态机生成器(DFA)匹配最优的指令和操作数,通过指令的属性,如指令的访存代价、流水线结构等众多属性分析每种指令的优劣并作出最优匹配选择,这些重要的属性都是通过国产处理器的结构描述文件(Architecture Description File,AD文件)通过结构描述语言编译器(ADL)编译生成获得;接着进行机器平台相关的优化,如寄存器分配、窥孔优化等,直至最后生成热方法的本地机器代码,服务器版编译器寄存器分配是一个全局图着色分配器,它可以充分利用处理器的大寄存器集合。
(二)结构描述文件
结构描述文件(Architecture Description File,AD文件)的准确描述,对服务器版即时编译器的移植工作非常关键,这是性能版虚拟机高性能的基础。它描述目标处理器的结构,并通过专门的ADL编译器将结构描述文件创建为JIT包含的结构相关优化源码,以便JIT生成高效正确的本地代码。
AD文件描述了三类基本的不同结构特征:目標平台的指令集(包括操作数)、寄存器(以及寄存器分配相关信息)以及针对调度优化的目标平台流水线结构信息,另外还有部分为了简化描述而增加的一些辅助定义。
1. 寄存器描述
寄存器格式的定义如下:
“reg_def” name ( register save type, C convention save type,
ideal register type, encoding, vm name );
函数的各个参数意思明确,“save type”表示寄存器分配寄存器在方法调用之间的保留类型,有不保存、调用处保存、调用前保存及调用前和调用处都保存等四种类型;第三个参数“ideal register type”用来确定如何保留恢复一个寄存器,“encoding”是由于有寄存器扩展,用于表示放置在opcodes中实际的位数。
寄存器描述中还包括reg_class和alloc_class,如整形寄存器、浮点寄存器、特殊寄存器(如标志寄存器)以及定义具有相同属性的寄存器类等,它们整个是为指令选择和寄存器分配提供信息,所有寄存器均是用户可见或普通指令涉及的寄存器,不包括特权或处理器内部寄存器。
2. 指令集描述
指令集在结构描述文件中的工作占着很大比重,一是由于申威平台指令较多,每条指令都需要描述;另一个是还需要描述操作码、操作数属性、考虑如何与中间表示匹配、汇编输出格式以及硬件执行上属于什么流水分类等。
下面我们以下两个具体实例来说明这些属性设置的含义:
实例1:
1. Instruct addI_reg_reg(iRegI dst, iRegI src1, iRegI src2) %{
2. match(Set dst (AddI src1 src2));
3.
4. size(4);
5. Format %{ “ADD $src1,$src2,$dst” %}
6. ins_encode %{
7. __ add($src1$$Register, $src2$$Register, $dst$$Register);
8. %}
9. ins_pipe(ialu_reg_reg);
10. %}
实例2:
1. Instruct addI_reg_imm13(iRegI dst, iRegI src1, immI13 src2) %{
2. match(Set dst (AddI src1 src2));
3.
4. size(4);
5. Format %{ “ADD $src1,$src2,$dst” %}
6. opcode(Assembler::add_op3, Assembler::arith_op);
7. ins_encode( form3_rs1_simm13_rd(src1, src2, dst) );
8. ins_pipe(ialu_reg_imm);
9. %}
instruct:指定機器指令的入口。实例中说明长字加指令的入口,前一个是指操作数都为寄存器时addl指令的入口,后面的指操作数一是寄存器一是13位立即数时addl指令的入口。
match:指定指令的中间表示匹配规则。
size:指令默认长度,申威处理器指令长度均为32位。
encode:指定指令的编码规则。在instruct定义中,有两种方式生成本地机器码,一种是通过ins_encode语句块直接指定指令序列;另一种是通过opcode和ins_encode配合完成,opcode指定指令的操作码(包括主操作码和辅助操作码),ins_encode指定指令的其他部分如何编码,这是通过enc_class来完成。
enc_class定义了若干指令编码类供instruct使用,通过这种方式,可把编码规则相同的指令用一个enc_class代替,这样可有效降低instruct定义的工作量且可降低出错概率。
ins_pipe:指定当前指令使用哪些pipe_class。这在处理器流水线的特征里进行了描述,包括指令长度、是否有延迟槽等的属性描述、指令执行部件资源描述和不同类型的操作涉及了流水线的哪些阶段的流水分类描述等。
format:汇编指令的输出格式。若为多条指令,应一起输出。
指令描述的其他信息描述也都比较简单,从文档中能够明显看出,这里不作详述。指令集定义完成被ADL编译器生成后,在指令选择阶段利用一个DFA来选择匹配最优的操作数和指令。
3. 流水线结构
结构描述文件还定义了处理器流水线的特征,它描述了指令执行在硬件流水线上的特征。该部分描述涉及四部分:
属性(attributes):定义了指令长度、是否有延迟槽等。
资源(resources):定义指令执行的功能部件。
流水线描述(pipe_desc):定义流水线站台。
流水线分类(pipe_class):定义不同类型的操作涉及了流水线的哪些阶段,,在指令集描述中的ins_pipe就是用于指定指令属于哪个流水线分类。
(三)指令匹配
在解析Java字节码并生成IR,进而形成抽象语法树(Abstract Syntax Tree, AST)中间文件,树中的节点表示诸如加减乘除等操作,其子节点表示其输入操作数,比如图4(a)表示寄存器形式的加法,该加法有两个整数寄存器(iRegI)操作数和一个标志寄存器操作数。这个树最终可简单对应到一条本地的addl指令。
如果节点包含子树,形成嵌套,则可表示更复杂的操作,如图4(b)所示,mull有3个操作数,其运算结果又作为addl的操作数之一。该树可能生成mull和addl两条本地指令,也可能生成1条乘加指令。对于更复杂的树,生成本地指令时可能面临更复杂的多样化选择。为了生成最优的指令,Hotspot中采用自底向上重写系统(bottom-up rewrite system,BURS)算法[7],根据目标机器体系结构描述,如本地指令与中间指令的关系、指令开销等自下而上匹配AST,选择最优本地指令。
四、结束语
Java虚拟机与体系结构密切相关,涉及处理器、操作系统核心、编译器、基础运行时库等模块,涉及机器码、汇编、C、C++、Java语言,其自身结构复杂、技术先进、代码庞大。为了满足企业级应用对性能的最高要求,即时编译模式不断采用大量静态编译和激进编译优化技术,同时又尽可能降低运行时开销。
C2即时编译器是一个专门面向服务器典型应用的充分优化过的先进自适应编译器,强调的是程序运行的峰值性能。它支持许多和C++编译器支持的相同类型的优化,以及一些传统编译器所不能做的自适应优化,如虚拟方法调用间的积极乐观内联(aggressive optimistic inlining)。与静态编译器相比,自适应编译器是非常灵活的,在优化层面上有著无可比拟的竞争优势,典型情况下甚至比先进的静态分析和编译技术更优秀、输出的代码质量更高。
本文深入分析HotSpot虚拟机的运行机制和C2即时编译框架,为下一步深入面向平台进行软硬件协同优化打下好的基础。
作者单位:郑艳 无锡城市职业技术学院
参 考 文 献
[1]周志明. 深入理解Java虚拟机-JAVA虚拟机高级特性与最佳实践(第2版). 机械工业出版社, 2013.6
[2] Tim Lindholm, etc. The Java? Virtual Machine Specification. Oracle Corporation, Inc.
[3] Peter B. Kessler. Java HotSpot? Virtual Machine. http://openjdk.java.net/groups/hotspot/docs/FOSDEM-2007-HotSpot.pdf, 2007
[4] Sun Microsystems. The Java HotSpot Virtual Machine. A Technical White Paper.
[5] Michael Paleczny, Christopher Vick, and Cliff Click. The Java HotSpot Server Compiler. In Proceedings of the Java Virtual Machine Research and Technology Symposium, 1-12,USENIX, 2001
[6] ADL语法规范. JavaSoft HotSpot Architecture Description Language Syntax Specification. 1997.9
[7]Eduardo Pelegrí-Llopart, Susan L. Graham. Optimal Code Generation for Expression Trees: an Application BURS Theory. In Proceedings of the 15th ACM Symposium on Principles of Programming Languages, ACM Press, 294-308, 1988