首页 > 代码库 > 运行期优化

运行期优化

    在部分商用虚拟机中,Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行地特别频繁,就会把这些代码块认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT编译器)


 

解释器和编译器

    当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。当程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。//解释执行节约内存,编译执行提升效率

    解释器还可以作为编译器激进优化时的一个“逃生门”。让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立时,退回解释状态继续执行。因此在整个虚拟机执行架构中,解释器和编译器经常是相辅相成配合工作的。

    HotSpot虚拟机中内置了两个即时编译器,称为Client Compiler(C1编译器)和 Server Compiler(C2编译器)。HotSpot虚拟机中默认采用解释器与其中一个编译器直接配合的方式工作,程序使用哪个编译器,取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与宿主及其的硬件性能自动选择运行模式,用户也可以使用-client或-server参数去强制指定虚拟机运行在Client模式还是Server模式。

    Mixed Mode 混合模式

    Interpreted Mode解释模式  -Xint 此时编译器完全不介入

    Compiled Mode编译模式   -Xcomp 解释器会在编译无法进行的情况介入

编译对象和触发条件

    1.运行过程中会被JIT编译的热点代码有两类

    1.1 被多次调用的方法

    编译器会对整个方法进行编译,这是虚拟机标准的编译方式

    1.2 被多次执行的循环

    编译器依然会以整个方法作为编译对象,而这种编译方式发生在方法执行过程中,所以被称为栈上替换(OSR)

    2.热点探测的两种方式

    2.1基于采样的热点探测,虚拟机周期性地检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,就认为是“热点方法”。优点:简单高效,明确方法调用关系 缺点:容易因为线程阻塞或别的外界因素的影响而扰乱热点探测。

    2.2基于计数器的热点探测,虚拟机为每个方法建立计数器,执行次数超过某个阈值,就认为是“热点方法”。优点:统计结果严谨 缺点:需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系。 

    HotSpot虚拟机采用的是第二种,为每个方法准备了两个计数器:方法调用计数器以及回边计数器。这两个计数器都有一个确定的阈值,当计数器超过阈值溢出就会触发JIT编译。

编译优化技术

    JDK设计团队几乎把对代码的所有优化措施都集中在了即使编译器上。所以一般来说即时编译器产生的本地代码会比javac产生的字节码更优秀。

语言无关的经典优化技术:公共子表达式消除

语言相关的经典优化技术:数组范围检查消除

最重要的优化技术之一:方法内联

  • 消除了方法调用的成本
  • 为其他优化手段建立了良好的基础
  • 理论上讲只有私有方法、实例构造器、父类方法、静态方法以及final修饰的方法,才能放心的进行内联
  • 如果遇到虚方法,就会向CHA(类型继承关系分析)查询此方法在当前程序下是否有多个目标版本可供选择,如果只有一个,可以进行内联,但是也属于激进优化,需要预留一个逃生门。如果程序的后续执行过程架子啊了导致继承关系发生变化的新类,就要抛弃已经编译的代码,退回解释状态执行,或者重新编译。
  • 如果CHA查询到多个版本的目标方法,则编译器还会使用内联缓存来完成内联,当第一次调用发生后,缓存记录下方法接收者的版本信息,如果每次进行的方法调用版本都是一样的,那这个内联还可以一直使用。但是如果方法接收者不一致,这时候会取消内联,查找虚方法进行方法分派。

最前沿的优化技术之一:逃逸分析(这项技术暂时还不成熟)

      当一个对象在方法里面被定义后,它可能被外部方法所引用,这种行为称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,这种行为称为线程逃逸。

     如果能够证明一个对象不会发生逃逸,则可以为这个变量进行一些高效的优化,如:

  • 栈上分配,此时对象会随着方法的结束而自动销毁,垃圾收集系统的压力会小很多。
  • 同步消除,如果一个对象不会逃逸,那么该变量无法被其他线程访问,所以对其进行的读写就不会有竞争,对这个变量实施的同步措施就可以消除。
  • 标量替换,把一个java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫标量替换,如果对象不会逃逸,而且可以拆分,那么程序真正执行的时候可能不创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来替代。

Java与C/C++编译器的对比

  • 即时编译器运行占用的是用户程序的运行时间,具有很大的时间压力,能采用的优化手段有限。
  • java是动态的类型安全语言,意味着虚拟机必须频繁地进行动态检查,如实例方法访问时检查空指针、数组元素访问时检查上下界范围、类型转换时检查继承关系。
  • java中大多数方法都是虚方法,这意味着java即时编译器在进行一些优化(如方法内联)时的难度要远远大于C++等静态优化编译器
  • Java语言可以动态扩展,运行时加载新的类可能改变程序类型的继承关系,这使得很多全局的优化都难以进行。
  • Java语言中对象的内存分配都是在堆上进行的,而C++对象则有多种内存分配方式,将减轻内存回收的压力。

     Java语言的这些性能上的劣势都是为了换取开发效率上的优势而付出的代价。而且即时编译器也有静态编译器不能做的优化,由于C++编译器的静态特性,以运行期性能监控为基础的优化措施都不能进行,如调用频率预测、分支频率预测等。    

运行期优化