深入理解JVM(十)——晚期(运行期)优化

对效率的追求是程序员天生的坚定信仰

当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些认定为热点代码(Hot Spot Code),为了提供热点代码的执行效率,运行时,虚拟机会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,JIT编译器)

解释器与编译器

许多主流的商用虚拟机都同时包含解释器和编译器,两者各有优势,当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行之后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获得更高的执行效率。
另外解释执行节约内存,编译执行提升效率。

HotSpot的两个即时编译器

Client Compiler和Server Compiler,简称为C1编译器和C2编译器,HotSpot虚拟机默认采用解释器与其中一个编译器直接配合的方式工作。虚拟机根据自身的版本以及宿主机器的硬件性能自动选择,也可以通过参数-client或者-server去强制指定。

即时编译本地代码需要占用程序时间,要编译优化程度更高的代码,解释器要替编译器收集性能监控信息,这对解释器执行速度也有影响。为了在程序响应速度与运行效率之间达到平衡,HotSpot采用分层编译的策略。
第0层,程序解释执行,不开启性能监控功能(Profiling),可触发第1层编译。
第1层,也称C1编译,将字节码编译为本地代码,进行简单,可靠的优化,可以加入性能监控的逻辑。
第2层,也称C2编译,也是将字节码编译为本地代码,但是会启动一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

热点代码

  • 被多次调用的方法
  • 被多次执行的循环体

第一种情况,由方法调用触发的编译,编译器会将整个方法作为编译对象,这种编译也是虚拟机标准的JIT编译方式。
第二种情况,虽然是循环体触发的编译,但是编译器依然会以整个方法体作为编译对象。这个编译方式因为编译在方法执行之中,因此形象地称之为栈上替换(On Stack Replacement,OSR,即方法栈帧还在栈上,方法就被替换了)

热点探测

热点探测并不一定知道方法具体被调用的次数,目前热点探测的方式有两种

  • 基于采样的热点探测
    周期性的检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,则是热点方法。该方式实现起来简单,有效,还可以获取调用关系,但是无法精准确定热度,容易受线程阻塞和别的外界因素影响。
  • 基于计数器的热点探测
    为每个方法建立一个计数器,统计方法的执行次数,执行超过一定的阀值,则是热度方法。

HotSpot采用第二种方式,为每个方法准备了两类计数器,方法调用计数器和回边计数器(循环体代码执行的次数),这两个参数都有阀值,超过时会触发JIT。
默认方法调用计数器client模式下是1500,Server模式下是10000。

编译过程

默认情况下,无论是方法调用还是OSR产生的编译请求,虚拟机在编译器未完成编译之前都是解释执行的,编译的动作则在后台的编译线程中进行。
client Compiler是一个简单快速的三段式编译器,主要关注点在于局部性的优化,而放弃很多耗时较长的全局优化手段。
第一阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(HIR),HIR使用静态单分配的形式来代表代码值,。在此之前编译器会在字节码上做一下基本的优化,如方法内联,常量传播等。
第二阶段,一个平台相关的后端,从HIR中产生低级中间代码表示(LIR),优化如空值检查消除,范围检查消除等。
最后阶段,是在平台相关的后端使用线性扫描算法,在LIR上分配寄存器,并在LIR上做窥孔优化,产生机器码。

Server Compiler是充分优化过的高级编译器,几乎能达到GUN C++编译使用的-O2参数时的优化强度,会执行所有经典的优化动作,如无用代码消除,循环展开,循环表达式外提,消除公共子表达式,常量传播,基本块重排序等。
还有与Java语言密切相关的优化技术,范围检查消除,空值检查消除等。

JIT编译优化实例

申明,编译优化对程序员是透明的,代码优化变换是建立在代码的某种中间表示或者机器码之上,绝不是建立在Java源码之上,以下只是为了说明

优化前代码

static class B {
    int value;
    final int get(){
        return value;
    }
}
public void foo(){
    y = b.get();
    //do stuff
    z = b.get();
    sum = y + z;
}

方法内联——去除方法调用的成本(栈帧建立等),为其它优化建立基础

public void foo(){
    y = b.value;
    //do stuff
    z = b.value;
    sum = y + z;
}

冗余访问消除(假设代码do stuff不会修改value的值)

public void foo(){
    y = b.value;
    //do stuff
    z = y;
    sum = y + z;
}

复写传播——这段代码并没有必要使用额外的变量z,它与y是完全相等的

public void foo(){
    y = b.value;
    //do stuff
    y = y;
    sum = y + y;
}

无用代码消除——可能是永远不会执行的代码,也可能时无意义的代码

public void foo(){
    y = b.value;
    //do stuff
    sum = y + y;
}

公共子表达式消除

语言无关的经典优化技术之一

如果一个表示式E已经计算过,并且从先前的计算到现在E的变量的值都没有发生变化,那E的这次出现就成了公共子表示式。对于这种表达式没必要计算,直接用之前的值代替就可以了。
这种优化若仅限于程序的基本块内,则称为局部公共子表示式消除,若覆盖多个基本块,则为全局公共基本块消除。

数组范围检查消除

语言相关的经典优化技术之一

Java语言在访问数组时系统会自动进行上下边界的范围检查,i>=0&&i< foo.length,否则抛出异常,对于程序来说很正常,但是虚拟机的执行子系统每次数组元素的读写都要带一次隐性的条件判断操作,无意是一种性能负担。
为了安全数组边界检查是一定要做的,但不是运行时一次不漏的检查。如:数组下标是一个常量,只要在编译期根据数据流分析来确定length的值,并判断常量是否越界,执行期就不用判断;更多的在循环中,使用循环变量访问数组,编译器只要更加数据流分析就可以判断循环变量的取值访问,就可以消除检查。

方法内联

最重要的技术之一

是编译器最重要的优化手段之一,除了消除方法滴用的成本之外,更重要是为其它优化建立良好的基础。

方法内联看似简单,将目标方法复制到发起调用的方法中,避免真实的方法调用。

Java的方法解析和分配中,只有invokespecial指令调用的私有方法,实例构造器,父类的方法以及invokestatic指令调用的静态方法才能在编译期进行解析,其它方法都需要在运行时进行方法的多态选择,并且可能存在多于一个版本的方法接收者。

对于虚方法,编译期做内联的时候无法确实应该使用哪个方法版本,而Java面向对象编程,默认的就是虚方法,而且鼓励用虚方法完成程序逻辑。

为了解决虚方法内联的问题,引入了“类型继承关系分析”(CHA)的技术,是一种基于整个应用程序的类型分析技术,用于确定目前加载的类中,某个接口是否有多于一种实现,某个类是否有子类,子类是否为抽象类等。

编译器进行内联时,如果是非虚方法就直接内联;虚方法,则向CHA查询,此方法是否有多个目标版本,只有一个版本,也可以进行内联;多个版本,编译器会进行内联缓存来完成,即在未发生方法调用前,内联缓存状态为空,第一次调用后,缓存下方法接收者的版本信息,并且每次都比较,如果版本一直一样,这个方法内联还可以一直用下去;如果不一样,则说明使用了多态的特征,取消内联,查找虚方法表进行分派。

逃逸分析

最前沿的优化技术之一

不是直接优化代码的手段,而是为其它优化手段提供依据的分析技术。

逃逸分析的基本行为是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,如作为调用参数传递到其它方法中,称为方法逃逸。也可能被其它外部线程访问,譬如赋值给类变量或其它线程中访问的变量。,称为线程逃逸。

如果证明一个对象不会逃逸,则可以为这个变量做高效的优化。

  • 栈上分配
    虚拟机中在Java堆中分配创建对象的内存空间是常识,但是如果该对象不会逃逸到方法外,那么栈上分配内存就是不错的注意,对象占用的内存就会随栈帧出栈而销毁,不用在堆上等GC。一般应用中,局部对象占的比率很大。

  • 同步消除
    线程同步本身就是相当耗时的操作,,如果一个变量不会逃逸出线程,那么这个变量的读写就不会有竞争,对这个变量实施的同步操作也可以消除。

  • 标量替换
    标量是指一个数据已经无法分解成更小的数据来表示了,JVM的原始数据类型及reference类型,都称为标量。

    如果把Java对象拆解,根据程序访问情况,将其使用到的成员变量恢复原始数据类型来访问就叫做标量替换。

    如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散,那程序执行的时候可能不创建这个对象,而是直接创建若干个被这个方法使用到的成员变量来代替。

猜你喜欢

转载自blog.csdn.net/rickey17/article/details/77113810