设为首页 加入收藏

TOP

Java 虚拟机 11 :运行期优化(二)
2018-04-03 09:08:28 】 浏览:261
Tags:Java 虚拟 运行 优化
地说是运行在Java虚拟机上得所有语言)本身继续拧的优化技术,下面主要看几项最有代表性的优化技术:

  • 语言无关的经典优化技术之一:公共子表达式消除
  • 语言无关的经典优化技术之一:数组范围检查消除
  • 最重要的优化技术之一:方法内联
  • 最前沿的优化技术之一:逃逸分析

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>=0&&i<foo.length这个条件,否则将会抛出一个数组下标越界异常。这对开发者来说是一件好事,即使程序员没有专门编写防御代码,也可以避免大部分的溢出攻击,但是对于虚拟机来说,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,无疑也是一种性能负担。

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

3、方法内联

最重要的优化手段之一。它的目的主要有两个:去除方法调用的成本(如建立栈帧等)、为其他优化建立了良好的基础,方法内联膨胀之后可以便于在更大范围上采取后续的优化手段。方法内联举个例子:

public final int getA()
{
    getA()语句1;
    getA()语句2;
    getA()语句3;
    getA()语句4;
    getA()语句5
}

优化之后变为:

public static void main(String[] args)
{
    main语句1;
    main语句2;
    int i = getA();
    main语句3;
    main语句4
}

从效果上看,无非是把getA()方法中的内容原封不动地拿到main函数中,但这样却少了保护现场、恢复线程、建立栈帧等一系列的工作,并且代码一膨胀,原来方法A有5行代码,方法B有6行代码,方法C有7行代码,对于三个方法各自运行来说可能没什么好优化的,但是三个方法合起来放到main函数之中,就有了很大的优化空间了。

讲到这里,我们是否理解为什么要尽量把方法声明为final?因为Java有多态的存在,运行时调用的是哪个方法可以根据实际的子类来确定,极大地增强了灵活性,但是这样的话,编译期间同样也无法确定应该使用的是哪个版本,所以无法被内联。但是被声明为final的方法不一样,这些方法无法被重写,所以调用类A的B方法,运行时调用的必然是类A的B方法,可以被内联。

4、逃逸分析

目前Java虚拟机中比较前沿的优化技术,它并不是直接优化代码的手段,而是为其他优化手段提供了分析技术。

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

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

(1)栈上分配

Java虚拟机中,对象在堆上分配这个众所周知。虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但回收动作无论是筛选可回收对象还是回收和整理内存都要耗费时间。如果确定一个对象不会逃逸出方法之外,那么让这个对象在栈上分配将会是一个不错的主意,对象所占用的内存空间就可以随着栈帧出栈而销毁,这样垃圾收集系统的压力将会小很多

(2)同步消除

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

(3)标量替换

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

关于逃逸分析的论文1999年就已经发表,但直到Sun JDK1.6才实现了逃逸分析而且直到现在这项优化尚未足够成熟,仍有很大改进余地。不成熟的原因主要是不能保证逃逸分析的性能收益必定能高于它的消耗。虽然在实际测试结果中,实施逃逸分析后的程序往往能运行出不错的成绩,但是在实际的应用程序,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况,或因分析过程耗时但却无法有效判别出非逃逸对象而导致性能有所下降。

如果有需要,并且确认对程序运行有益,可以使用参数-XX:+DoEscapeAnalysis来手动开启逃逸分析,开启之后可以通过参数-XX:+PrintEscapeAnalysis来查看分析结果。有了逃逸分析支持之后,就可以使用参数-XX:+EliminateAllocations来开启标量替换,使用参数-XX:+EliminatLocks来开启同步消除,使用参数-XX:+PrintEliminateAllocations查看标量的替换情况。

尽管目前逃逸分析技术仍不是十分成熟,但是在今后的虚拟机中,逃逸分析技术肯定会支撑起一系列实用有效的优化技术。

首页 上一页 1 2 下一页 尾页 2/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Java 虚拟机10:类加载器 下一篇Java 虚拟机 13:互斥同步、锁优..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目