JVM运行期优化

介绍四种最具有代表性的优化技术:

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

如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果代替E就可以了。如果这种优化仅限于程序的基本块内,便称为局部公共子表达式消除,如果这种优化的范围涵盖了多个基本块,那就称为全局公共子表达式消除。

例如:

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

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

int d = (a + E) + a + E;

这时,编译器还可能进行另外一种优化:代数化简,把表达式变为:

int d = a * 2 + E * 2;

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

数组边界检查消除是即时编译器中的一项语言相关的经典优化技术。java是一门动态安全的语言,对数组的读写访问也不像C、C++那样在本质上是裸指针操作。如果有一个数组array[],在java语言中访问数组元素array[i]的时候系统将会自动进行上下界的范围检查,即检查i必须满足i>=0&&i<array.length这个条件,否则将抛出一个运行时异常:java.lang.ArrayIndexOutOfBoundsException。这对软件开发者来说是一件很好的事情,即使程序员没有专门编写防御代码,也可以避免大部分的溢出攻击。但是对于虚拟机的执行子系统来说,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,这无疑也是一种性能负担。

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

将这个数组边界检查的例子放在更高的角度来看,大量的安全检查令编写java程序比编写C/C++程序容易很多,如数组越界会得到ArrayIndexOutOfBoundsException异常,空指针访问会得到NullPointException,除数为零会得到ArithmeticException等,在C/C++程序中出现类似的问题,一不小心就会出现Sagment Fault信号或者Window编程中常见的“xxx内存不能为Read/Write”之类的提示,处理不好程序就直接崩溃退出了。但这些安全检查也导致了相同的程序,java要比C/C++做更多的事情,这些事情就成为一种隐式开销,如果处理不好它们,就很可能成为一个Java语言比C/C++更慢的因素。要消除这些隐式开销,除了如数组边界检查优化这种尽可能把运行期检查提到编译器完成的思路之外,另外还有一种避免思路——隐式异常处理,java中空指针检查和算术运算中除数为零的检查都采用了这种思路。

例如:

if(obj != null){
    return obj.value;
}else{
    throw new NullPointException();
}

在使用隐式异常优化之后,虚拟机会把上面伪代码所表示的访问过程变为如下伪代码:

try{
    return obj.value;
}catch(sagment_fault){
    uncommon_trap();
}

虚拟机会注册一个Segment Fault信号的异常处理器,这样当obj不为空的时候,对value的访问是不会额外消耗一次对obj判空的开销的。代价就是当obj真的为空时,必须转入到异常处理器中回复并抛出NullPointException异常,这个过程必须从用户态转到内核态中处理,结束后再回到用户态,速度远比一次判空检查慢。当obj极少为空的时候,隐式异常优化是值得的,但假如obj经常为空的话,这样的优化反而会让程序更慢。

与程序语言相关的其他消除操作还有不少,如自动装箱消除、安全点消除、消除反射等。

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

方法内联行为很简单,不过是把目标方法的代码“复制”到发起调用的方法之中,避免发生真实的方法调用而已。

函数调用实际上是将程序执行顺序转移到该函数所存放在内存中的某个地址,将函数的内容执行完之后,再返回去执行该函数调用之后执行的部分。但是这种转移操作要求在转移之前要保存执行现场,返回去要恢复现场,因此函数调用有一定的时间开销和空间开销。对于那种代码块不是很大却又频繁调用的函数来说,这个时间和空间的消耗会很大。

例如如下伪代码:优化之前:

static class B {
    int value;
    final int get(){
        return value;
    }
}

public void test(){
    y = b.get();
    //do something
    z = b.get();
    sum = y + z;
}

方法内联优化之后:

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

冗余访问消除:

如果注释掉的部分“do something”执行之后,b.value的值并不会发生变化,就可以进行如下优化:

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

复写传播:

将程序中没有必要使用的变量去掉,比如没有必要使用变量“z”,可以进行如下优化:

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

无用代码消除:

无用代码就是永远不会被执行的代码,也可能是完全没有意义的代码,可以进行如下优化:

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

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

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

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

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

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

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

参考文档:《深入理解java虚拟机》周志明著

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值