《深入理解java虚拟机》---晚期(运行期)优化(11)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hy_coming/article/details/82502571

一、概述

在部分的商用虚拟机中,java虚拟机最初是通过解释器进行解释执行的,当虚拟机发现某个方法或者代码的运行特别频繁时,就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机就会把这些代码编译成与本地平台无关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT编译器)。这个编译器其实并不是虚拟机必需的部分,但是确实衡量一款商用虚拟机优秀与否的最关键标志之一,也是虚拟机中最核心且最能体现虚拟机技术水平的部分。

二、HotSpot中的即时编译器

1.解释器和编译器

现在很多主流的商用虚拟机都是采用解释器和编译器并存的架构,如HotSpot、J9等。当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行,在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获得更高的执行效率,所以当程序运行环境中内存资源限制较大(如嵌入式系统中),可以使用解释执行节约内存,反之可以使用编译执行来提高效率。同时,解释器还可以作为编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候能够提升运行速度的优化手段。没有解释器的虚拟机也会采用不进行激进优化的C1编译器担任“逃生门”。

这里写图片描述

HotSpot虚拟机中除了解释器之外还内置了两个即时编译器,分别称为Client Compiler和Server Compiler,或者称为C1编译器和C2编译器(Opto编译器),在运行的时候虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,当然也可以使用“-client”和“-server”参数去强制指定虚拟机运行在Client模式或者Server模式下。还可以用参数“-Xint”和“-Xcomp”指定虚拟机在解释模式或者是编译模式下运行,其中编译模式下,只有当编译无法进行的情况下,解释器才会介入执行。

但是由于即时编译器编译本地代码需要占用程序运行时间,为了编译出与优化程度更高的代码,解释器可能还需要替编译器收集性能监控信息,但是为了保证解释器的执行速度,在JDK1.6之后采用了分层编译的策略

  • 第0层,程序解释执行,不开启性能监控功能
  • 第1层,也称C1编译,将字节码编译成本地代码,进行简单、可靠的优化,如有必要就加入监控逻辑
  • 第2层,也称C2编译,也是讲字节码编译成本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化

实施分层编译后,Client Compiler和Server Compiler将会同时工作,许多代码可能会多次编译,用Client Compiler获得更高的编译速度,用Server Compiler获得更好的编译质量,在解释执行的时候也无需在承担收集性能监控信息的任务。

2.编译对象与触发条件

上面说到的“热点代码”指的的是多次被调用的方法或者对次被执行的循环体,前面一种会触发JIT编译方式,后一种会触发OSR编译(栈上替换,其实也是以整个方法作为编译对象,并且发生在方法执行过程中),这两种编译方式的触发触发条件主要采用下面两种热点探测方式

  • 基于采样的热点探测:采用这种方法的虚拟机会周期性的检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那么判定方法为“热点方法”,这种方式简单高效,但是精确度不够
  • 基于计数器的热点探测:虚拟机会为每个方法(甚至代码块)建立计数器,统计方法的执行次数,当值超过设置的阈值就认为是“热点方法”,这种方式更加准确和严谨

HotSpot虚拟机使用的是基于计数器的热点探测方法,为每个方法准备了方法调用计数器和回边计数器,方法调用计数器在Client模式下是1500次,在Server模式下是10000次,有-XX:CompileThreshold参数可以设置。当一个方法别调用时会先检查是否有被编译过的版本,有就直接用本地代码执行,没有调用计数器加1,知道调用计数器和回边计数器的总和超过方法调用计数器的阈值,这是会向即时编译器发出请求,但是不会停下来等待请求完成,而是按照解释方式执行字节码,直到提交的请求被编译器编译完成。当编译工作完成之后,这个的方法的调用入口就会自动改成新的,其实这个调用计数器统计并不是调用的绝对次数,而是一个相对的执行频率,当超过一定的时间限度,如果方法的调用的次数任然不足以让他提交给即时编译器编译,那么这个调用计数器就会减少一半,叫做计数器热度的衰减,这段时间就称为此方法统计的半衰周期,这个动作是在虚拟机进行垃圾收集时顺便进行的,设置关闭。

这里写图片描述

回边计数器的阈值在Client模式下是13995,在Server模式下是10700,当解释器遇到一条回边指令时,先查找有没有编译好的版本,有就执行已经编译的,没有回边计数器加1,然后判断调用计数器和回边计数器之和是不是超过回边计数器的阈值,超过就会提交OSR编译请求,并把回边计数器的值降低一点,以便继续在解释器中循环执行,直到编译器输出编译后的结果,没有热度衰减的过程,计算就是方法循环体的绝对次数。当回边计数器溢出的时候,还会将方法计数器的值也调整到溢出状态,这样下次再进入该方法的时候就会执行标准编译过程。

这里写图片描述

3.编译过程

前面已经说多,在编译器还未完成之前,都是仍然按照解释方式继续执行的,而编译动作在后台的编译线程中进行。Client Compiler后台编译过程如下图

  • 第一个阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(HIR),在此之前编译器会在字节码上完成一部分基础优化,如方法内联、常量传播等优化将会在字节码被构造成HIR之前完成
  • 第二个阶段,一个平台相关的后端从高级中间代码表示(HIR)中产生低级中间代码表示(LIR),在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等。
  • 第三个阶段,平台相关的后端使用线下扫描算法,在LIR上分配寄存器,并在LIR上做窥空优化,然后产生机器代码

Server Compiler是专门面向服务端的典型应用并未服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器,几乎达到GUN C++编译器使用-O2参数时的优化强度,它会执行所有经典的优化动作,如无用代码消除,循环展开,循环表达式外提,消除公共子表达式,常量传播,基本块重排序等。还会实施一些与Java语言特性密切相关的优化技术,如范围检查消除,空值检查消除。

4.查看及分析即时编译结果

一般来说,虚拟机的即时编译过程对用户程序时完全透明的,虚拟机通过解释执行代码还是编译执行代码,对于用户来说并没有什么影响。但是虚拟机也提供了一些参数用来输出即时编译和某些优化手段(如方法内联)的执行状态。

三、编译优化技术

因为虚拟机的设计团队把几乎所有的优化措施都集中到了即时编译器之中,因此一般来说即时编译器产生的本地代码会比Javac产生字节码更加优秀。

  • 公共子表达式消除
  • 数组边界检查消除
  • 方法内联
  • 逃逸分析,目前还不是很成熟的技术

四、Java和C/C++的编译器对比

主要两者对比的即时编译器和静态编译器,下面说说即时编译器的劣势

  • 即时编译器占用的是用户程序的运行时间,时间成本比较大
  • java语言是动态的类型安全语言,这就意味着需要由虚拟机来确保程序不会违反语言语义或者访问非结构化内存,消耗时间成本大
  • java中没有virtual关键字,使用虚方法的频率高,优化难度大
  • java是可扩展语言,全局优化难度大
  • java语言中的内存分配是在堆上进行的,方法中的局部变量才会在栈上分配,内存回收压力大

同样是因为上面的劣势才造就了java语言的优势,也就是在开发效率上做出来很大的贡献。

猜你喜欢

转载自blog.csdn.net/hy_coming/article/details/82502571