JVM优化技术

Posted 再等三分钟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM优化技术相关的知识,希望对你有一定的参考价值。

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

公共子表达式消除是一个普遍应用于各种编译器的经典优化技术,它的含义是:如果一个表达式E已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式,对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果替代E就可以了,

举个例子简单说明它的优化过程,假设有如下代码

int d = (c*b)*12 + a +(a+b*c)

如果这段代码交给javac编译器则不会进行任何优化,那生成的代码如下:


当这段代码进入到虚拟机即时编译器后,它将进行如下优化:编译器检测到“c*b”与“b*c”是一样的表达式,而且在计算期间b与c的值是不变的,因此这条表达式就视为:

int d = E *12 + a +( a+E)

这时候编译器还可能(取决于哪种虚拟机编译器及具体的上下文)进行另外一种优化:代数简化,把表达式变为:

int d = E *13 + a *2;

2、语言无关的经典优化技术之一:数组范围检查消除

数组边界检查消除是即时编译器中的一项语言相关的的经典优化技术。java是一门动态安全的语言,对数组的读写访问不像c、c++那样本质上是裸指针操作,如果有一个数组foo[],在java语言中访问数组元素foo[i]的时候系统将会自动进行上下界的范围检查,即检查i必须满足i>=0&&i<=foo.length这个条件,否则将会抛出一个运行时异常:java

.lang.ArrayIndexOutOfBoundsException。

为了安全,数组边界检查肯定是必须的,但数组边界检查是不是必须在运行期间一次不漏的检查则是可以“商量”的事情,例如:数组下标是一个常量,foo[3],只要在编译期根据数据流分析来确定foo.length的值,并判断下标“3”没有越界,执行的时候就无须判断了,更常见的情况是数组访问发生在循环之中,并且使用循环变量来进行数组访问,如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远在区间[0.foo.length]之内,那在整个循环中就可以把数组的上下边界检查消除掉,这可以节省很多次的条件判断操作。

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

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

public static void foo(Object obj) {
    if(obj != null) {
        System.out.println("do sometthing");
    }
}
public static void testInline(String[] args) {
    Object obj = null;
    foo(obj);
}

事实上,testInline()方法的内部全部是无用代码,如果不做内联,后续即使进行了无用代码消除的优化,也无法发现任何“Dead Code”,因为如果分开来看,foo()和testInline()两个方法里面的操作都可能是有意义的。

方法内联的优化行为看起来很简单,不过是把目标方法的代码“复制”到发起调用的方法中,避免发生真实的方法调用而已,但实际上java虚拟机中的内联过程远远没有这个简单。因为如果不是即时编译器做了一些特别的努力,按照经典编译原理的优化理论,大多数java方法都无法进行内联!
无法内联的原因:只有使用iinvokespecial指令调用的私有方法、实例构造器、父类方法和使用invokestatic指令进行调用的静态方法才是在编译期进行解析的,除了上述四种方法之外,其他的java方法调用都需要在运行时进行方法接收者的多台选择,并且都有可能存在于一个版本的方法接收者,简而言之,java语言中默认的实例方法就是虚方法。

对于一个虚方法,编译器做内联的时候根本就无法确定应该使用哪个方法版本,例如,优化前的代码:

static class B {

int value;

final int get() {

return value;

}

}

public void foo() {

y = b.get();

z = b.get();

sum = y +z;

}

内联后的代码:

public void foo(){

y = b.value;

//do stuff

z = b.value;

sum = y + z;

}

b.get()内联为b.value,就是不依赖上下文就无法确定b的实际类型是什么,假如ParentB和SubB两个具有继承关系的类,并且子类重写了父类的get()方法,那么要执行父类的get()方法还是子类的get()方法,需要在运行期才能确定,编译期无法得出结论。

java语言提倡使用面向对象的编程方式,而java对象的方法默认就是虚方法,因此java间接鼓励了程序员使用大量的虚方法来完成程序逻辑,内联合虚方法之间会产生“矛盾”?那该怎么办呢?

为了解决虚方法内联的问题,java虚拟机设计团队想了很多办法,首先是引入了一种名为“类型继承关系分析”的技术,这是一种基于整个应用程序的类型分析技术,它用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在于子类且子类是否为抽象类等信息。

编译器进行内联时,如果是非虚方法,那么直接进行内联就可以了,这是偶的内联时有稳定前提保障的,如果遇到虚方法,则会向CHA查询此方法在当前程序下是否有多个目标版本可供选择,需要预留一个“逃生门”,称为守护内联,如果程序的后续执行过程中,虚拟机一直没有加载到会令这个方法的接收者的继承关系发生变化的类,那么这个内联优化的代码就可以一直使用下去,但是如果加载了导致继承关系发生变化的新类,那就需要抛弃掉已经编译的代码,退回到解释状态执行,或者重新进行编译。

如果向CHA查询出来的结果是有多个版本的目标方法可供选择,则编译器还将会进行最后一次努力,使用内联缓存来完成方法内联,这是一个建立在目标方法正常入口之前的缓存,它的工作原理大致是:在未发生方法调用之前,内联缓存状态为空,当第一次调用发生后,缓存记录下方法接收者的版本信息,并且,每次进行方法调用时都比较接收者版本,如果以后进来的每次调用的方法接收者版本都是一样的,那这个内联还可以一直用下去,如果发生了方法接收者不一致的情况,就说明程序真正使用到了虚方法的多态特性,这时候才会取消内联,查找虚方法表进行方法分派。



4、最前沿的优化技术之一:逃逸分析

逃逸分析与类型继承关系分析一样,并不直接优化代码的手段,而是为其他优化手段提供依据的分析技术。

逃逸分析的基本行为就是分析对象动态作用域,:当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种行为称为方法逃逸,甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,这种行为称为线程逃逸。

如何证明一个对象不会逃逸到方法或线程之外,也就是被的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化,如:

栈上分配:java虚拟机中,在java堆多行分配对象内存,java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问堆中存储的对象数据。虚拟机的垃圾收集系统可以回收掉堆中不再使用的对象,但回收动作无论是筛选可回收对象,还是回收和整理内存都需要耗费时间。如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会是一个不错的注意,对象所占用的内存空间就可以随着栈帧出栈而销毁。在一般应用中,不会逃逸的局部对象所占的比率很大,如果能使用栈上分配,那大量的对象就会随着方法的结果而自动销毁了,垃圾收集系统的压力会小很多。

同步消除:线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其它现场访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉。

标量替换:标量是指一个数据已经无法再分解变成更小的数据了,java虚拟机中的原始数据类型(int long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量,相对的如果一个数据可以继续分解,那就把它称作聚合量,java中的对象就是最典型的聚合量,如果把一个java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象。而改为直接创建它的若干个被这个方法使用到的成员来代替,将对象拆分后,除了可以让对象的成员变量在栈上分配和读写之外,还可以为后续的进一步优化手段创建条件。

以上是关于JVM优化技术的主要内容,如果未能解决你的问题,请参考以下文章

JVM总结:晚期(运行期)优化

JVM优化技术

JVM优化之循环展开(附有详细的汇编代码)

分布式技术专题「系统服务优化系列」Web应用服务的性能指标优化开发指南(JVM篇)

不为人知的jvm编译优化技术,只有你知道

JVM中的 JIT 即时编译及优化技术