郭书超
(九江学院电子工程学院,江西 九江 332005)
即时编译器编译性能的好坏及代码优化程度的高低作为衡量商用java虚拟机的关键技术指标,同时也是虚拟机技术水平的最好体现。由于java虚拟机规范知识规定了字节码指令的动作,但并没有规定虚拟机的实现方式。执行引擎的核心动作就是不停读取字节码,解释(编译)执行,直到虚拟机进程的退出为止。Sun HotSpot虚拟机执行引擎为解释器与编译器共存的架构方式,内部的编译器是即时编译器主要由Client Compiler和Server Compiler构成,解释器与其中的一种构成混合模式的虚拟机执行引擎。
HotSpot的执行引擎采用解释器和即时编译器共存的架构,对于一般的代码采用解释器每次读取字节码指令,将指令解释乘本地代码并予以执行。这样机制能够有效节约内存,减少编译时间,让代码更加快速的进入执行状态,但是存在代码执行效率低的缺点。即时编译器采用热点代码侦测技术,实时把热点代码编译成本地代码,调用的时候优先使用本地编译过的代码,可以大大提高虚拟机的运行速度。另外不同的编译器,还能有效实现局部或全局的代码的优化,有效提高字节码的解释效率,节约程序的调用时间。
从java虚拟机角度观察,hotspot中类的加载分两种情况:一种是启动类加载器的加载器,由CPP代码实现;另外一种就是加载其他类的加载器。以下代码分析的都是在目录:/openjdk/hotspot/src/share/vm下,以下出现的目录都位于该目录之下。由于最开始java环境还没有,通过CPP代码构建编译的环境:
首先:hotspot启动时,根据运行环境的不同,决定使用的寄存器、指令集及缓存大小等,判断CPU架构类型,在sparc、x86、x86-64或arm等结构中选择,根据架构的不同加载不同的文件。
然后:进行加载过程的第一步—验证:
(1)格式的验证,主要验证文件的魔数是否正确、主次版本号是否合理、常量池中的常量内类是否合法、常量的索引是否符合、结构是否符合UTF8编码等。此时,如果常量池中的还有内容没有加载,便进行常量池的清理就会出现错误。
(2)元数据验证,主要是对字节码描述信息的语义进行分析,使得符合java语言的规范,主要包括类是否有继承,继承的父类是否能够被继承,该类是否为抽象类,类中的字段是否与父类的冲突等。
(3)字节码验证,主要是验证数据流和控制零分析,保证程序语义的正确,逻辑合理,实现虚拟机的安全运行。
(4)符号引用的验证,主要是解析阶段进行,对类的匹配信息验证。验证阶段也是非常重要的,若出现错误,根据不同的时段,会抛出不同的异常。
接着:使用类加载器实现类的加载,类加载器通过类的全限定名将描述该类的二进制字节流放置到java虚拟机。类加载器的和类本身都需要在虚拟机中是唯一存在的,每个加载器拥有自己的类命名空间。类加载过程中,如果发现制定的包已经被虚拟机加载,就根据加载信息直接使用加载过的包,同时对类调用的计数器值加1。同样的类加载器,结合不同的类加载,同样可以在虚拟机中存在,通过哈希算法,被标识成不同的值。类加载过程中主要是采用双亲委派模型,通过启动类加载器、扩展类加载器、应用程序加载器的共同配合进行加载。这种加载模式中,假设除了最顶层的类加载器外,其他的类都有父类加载器。在收到类加载请求之后,并不直接进行类的加载,将类加载的任务委派给父类加载器完成,由于每个类都是这样进行,所有的类加载请求都会被提交到Objcet的类加载,只有当父类无法加载时,子类才尝试自己加载类。
然后:虚拟机的运行。HotSpot虚拟机和主流的商用虚拟机一样都是采用解释器与编译器共存的架构。这种架构的优势体现在以下三个方面:
(1)在类刚加载时,首先工作在第0级,通过编译策略决定java方法的编译等级。此时主要由解释器对类解释执行,实现节约编译时间,达到立即执行的目标。随着类运行时间的累计,越来越多的代码都会被标记为热点代码,经编译器编译成本地代码,实现执行效率的提高。
(2)在代码提交编译到编译成功投入运行的时段中,代码的执行依旧靠解释器予以解释执行。
(3)在代码优化过程中,若是出现了优化失败的情况时,可以通过逆优化实现“代码逃逸”,解释器在此过程中充当着“逃逸门”的作用。在HotSpot虚拟机中使用不同的参数控制使用不同的即时编译器,将解释器和选定的即时编译器搭配使用是其工作的常态,使用“-Xint”参数实现虚拟机在解释模式下运行,老版本虚拟机可以通过参数“-Xcomp”强迫运行在编译方式中。
为了平衡程序启动的速度和运行效率,虚拟机采用了分层编译的手段达到两种编译器共同参与编译的目标。分层编译的核心是编译队列的应用,对与队列中的每个方法,JVM计算时间时间的发生率,每次出队的都是发生率最大的元素,使得过时的方法很快就可以删除掉。在解释器解释执行代码时,当虚拟机侦测到某个方法或代码块(主要是循环)执行非常频繁时,频繁程度主要采用基于采样的热点探测和基于计数器的热点探测两种方法来裁决,前者实现简单,容易受到外界影响,使用场合不多;后者结果更加准确,通过方法调用计数器和回边计数器的共同配合,实现热点代码的探测。
经过热点代码的认定之后,热点代码被调用时,虚拟机就会检查是否有被JIT编译的版本,存在就会优先使用编译后的代码运行;否则将方法调用计数器或回边计数器加上1,判断方法调用计数器和回边计数器的和是否超过计数器设定的阈值,如果超过阈值,就向即时编译器提交该方法的代码编译请求,在等待编译的时段内的代码继续以解释的方式执行。引入热点代码是为了提高热点代码的执行效率,运行时,虚拟机会将这些代码编译成与平台相关的机器码,将抽象的IR(中间表示)、CFG(控制流图)和SSA(静态单赋值)转变为具体的寄存器、编译目标内容,达到缩短编译时间实现代码优化的目标。
经过前期的准备工作,编译器选择java方法或循环体作为编译的目标。编译方法时,首先创建一个Compilation类,该类中的方法compile_mothod()被用来执行编译的过程,具体代码c1c1_compiler.cpp。明确将编译过程分成多个中间环节,甚至能够通过VM选项,得到非常详细的编译细节。打开VM选项后,可以得到CFG文件,该文件描述了编译的各个环节。
(1)生成HIR环节,HIR相当于基本块组成的控制流图。
(2)生成LIR环节,该环节中,编译器生成了寄存器分配前的LIR代码,相对与HIR环节,此处增加了LIR指令信息,局部变量的状态也发生了变化,变量名分配了虚拟寄存器。该处的寄存器是LIR格式的虚拟寄存器,明确了机器指令,甚至包括指令名称与寻址方式,通过分配物理寄存器明确实际地址即可。
(3)寄存器分配中为了充分利用寄存器资源,尽可能将程序变量尽量分配到寄存器中,达到提高执行速度的目标。如何将数据尽量长时间的保存在寄存器中,并将废弃的数据尽快清除是一个必须解决的问题。HotSpot使用了线性扫描算法,该算法的核心是:对任意两个变量的生命区间存在着重叠区域,不能将同一物理寄存器分配给这两个变量。HashSet.add()方法完成寄存器的分配任务。最后生成优化后的字节码。
经过经典优化如无用代码消除、循环展开、循环表达式外提、消除公共子表达式、块重排、常量传播等优化后的代码性能几乎可以达到GNU C++编译器的-O2参数的优化强度,说明基于热点探测的即时触发技术还是非常有效的优化手段。
本文通过研究HotSpot虚拟机类加载及优化的原理与代码实现,在深刻理解其工作原理基础上,加上对HotSpot代码的阅读,为自己理解虚拟机的工作原理与将来实现虚拟机打下良好的基础。
[1]陈涛著.HotSpot实战 [M].人民邮电出版社,2014(03).
[2]周志明著.深入理解Java虚拟机-JVM高级特性与最佳实践 [M].机械工业出版社,2014(04).
[3]Tim Lindholm、 Frank Yellin、Gilad Bracha、Alex Buckley著,周志明,薛笛,吴璞渊,冶秀刚 译 Java虚拟机规范(Java SE 7版)[M].机械工业出版社,2014(01).