您的位置:首页 > 编程语言 > Java开发

Java即时编译和逃逸分析

2014-04-15 10:30 387 查看
在Java编程语言和环境中,即时编译器(JIT compiler,just-in-time compiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。当你写好一个Java程序后,源语言的语句将由Java编译器编译成字节码,而不是编译成与某个特定的处理器硬件平台对应的指令代码(比如,Intel的Pentium微处理器或IBM的System/390处理器)。字节码是可以发送给任何平台并且能在那个平台上运行的独立于平台的代码。

最早的Java建置方案是由一套转译程式(interpreter),将每个Java指令都转译成对等的微处理器指令,并根据转译后的指令先后次序依序执行,由于一个Java指令可能被转译成十几或数十几个对等的微处理器指令,这种模式执行的速度相当缓慢。 针对这个问题,业界首先开发出JIT(just in time)编译器。当Java执行runtime环境时,每遇到一个新的类别(class:类别是Java程式中的功能群组),类别是Java程式中的功能群组-JIT编译器在此时就会针对这个类别进行编译(compile)作业。经过编译后的程式,被优化成相当精简的原生型指令码(native code),这种程式的执行速度相当快。花费少许的编译时间来节省稍后相当长的执行时间,JIT这种设计的确增加不少效率,但是它并未达到最顶尖的效能,因为某些极少执行到的Java指令在编译时所额外花费的时间可能比转译器在执行时的时间还长,针对这些指令而言,整体花费的时间并没有减少。 基于对JIT的经验,业界发展出动态编译器(dynamic compiler),动态编译器仅针对较常被执行的程式码进行编译,其余部分仍使用转译程式来执行。也就是说,动态编译器会研判是否要编译每个类别。动态编译器拥有两项利器:一是转译器,另一则是JIT,它透过智慧机制针对每个类别进行分析,然后决定使用这两种利器的哪一种来达到最佳化的效果。动态编译器针对程式的特性或者是让程式执行几个循环,再根据结果决定是否编译这段程式码。这个决定不见得绝对正确,但从统计数字来看,这个判断的机制正确的机会相当高。事实上,动态编译器会根据「历史资料」做决策,所以程式执行的时间愈长,判断正确的机率就愈高。以整个结果来看,动态编译器产生的程式码执行的速度超越以前的JIT技术,平均速度可提高至50%。

Hotspot的执行过程中组合了编译、性能分析以及动态编译。它并没有把所有要执行的代码全部都转化为机器码。只是编译
了“热门”代码(也就是执行最频繁的代码)。它会事先搜集性能分析数据,决定哪段代码执行足够频繁。然后编译该段代码,
这样就不必把时间浪费在编译哪些不经常执行的代码上了(这些代码可以在将要执行前编译)。从而编译器可以有更多的时间来
优化热门代码路径了。
Hotspot中提供了两个编译器,client版和server版的。默认采用client版的。可以在启动时指定-server参数来启动server
版的编译器。Server版针对最大峰值操作速度进行了优化,适用于需要长期运行的服务器应用程序。client版的优化目标,是减
少应用程序的启动时间和内存消耗,优化的复杂程度远远低于服务器编译器,因此需要的编译时间也更少。

2、JIT 即时编译

JIT是相对而言的,它不会先编译代码,只是在确定某个代码路径即将被执行时才编译。这样做的好处是可以加快java程序的
启动速度。因为在开始执行之前不需要冗长的编译了。同样的,象硬币的两面,JIT也有它天性的缺陷,没有足够的的时间来执行
编译优化了。在代码的执行效率上就相对要差些。

3. Java相对于c/c++慢的原因:

1、即时编译器运行占用的是用户的运行时间,具有很大的时间压力,它提供的优化手段严重受制与编译的成本。而编译的时间在静态编译中并不是主要关注点。
2、java语言是动态的类型安全语言,意味着由虚拟机确保程序不会违反语言的语义或访问非结构化内存。虚拟机需要频繁的动态检查,如空指针,数组越界,继承关系等,总体消耗不少时间。
3、java语言使用虚方法的频率远大于C/C++,意味对方法的接收者进行多态的选择频率远大于C/C++,意味着即时编译器的优化难度远远大于C/C++编译器。
4、java语言是动态扩展的语言,运行时会加载新的类,改变程序的继承关系,使得很多全局优化难以进行。只能采用激进的方式,在运行时撤销或重新进行一些优化。
5、java语言的对象是在堆上分配,只有方法的局部变量才在栈上分配。而C/C++语言有多种分配方式,既可以在堆上分配,又可以在栈上分配,减轻了内存回收的压力。另外C/C++语言主要由用户代码回收内存,不存在无用对象筛选的过程,效率要比垃圾回收机制要高。

Java语言在性能上的劣势都是为了换取开发效率上的优势而付出的代价。动态安全,动态扩展,垃圾回收这些“拖后腿”的特性都是为JAVA的开发效率做出了很大的贡献。
何况java即时编译器能做的,C/C++的静态优化编译器不一定能够做:
由于C/C++的静态编译,以运行性能监控为基础的优化措施它都无法进行,如调用频率预测,分支频率预测,裁剪未使用分支等,这些都是称为java语言独有的性能优势。

4. 逃逸分析:当变量(或者对象)在方法中分配后,其指针被返回或者被全局引用(这样就会被其他过程或者线程所引用),这种现象称作指针(或者引用)的逃逸(Escape)。

我们知道java对象是在堆里分配的,在调用栈中,只保存了对象的指针。

当对象不再使用后,需要依靠GC来遍历引用树并回收内存,如果对象数量较多,将给GC带来较大压力,也间接影响了应用的性能。减少临时对象在堆内分配的数量,无疑是最有效的优化方法。

怎么减少临时对象在堆内的分配数量呢?不可能不实例化对象吧!

场景介绍

其实,在java应用里普遍存在一种场景。一般是在方法体内,声明了一个局部变量,且该变量在方法执行生命周期内未发生逃逸(在方法体内,未将引用暴露给外面)。

按照JVM内存分配机制,首先会在堆里创建变量类的实例,然后将返回的对象指针压入调用栈,继续执行。

这是优化前,JVM的处理方式。

逃逸分析优化 - 栈上分配

优化原理:分析找到未逃逸的变量,将变量类的实例化内存直接在栈里分配(无需进入堆),分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。

这是优化后的处理方式,对比可以看出,主要区别在栈空间直接作为临时对象的存储介质。从而减少了临时对象在堆内的分配数量。

逃逸分析的原理很简单,但JVM在应用过程中,还是有诸多考虑。

比如,逃逸分析不能在静态编译时进行,必须在JIT里完成。原因是,与java的动态性有冲突。因为你可以在运行时,通过动态代理改变一个类的行为,此时,逃逸分析是无法得知类已经变化了。

逃逸分析另一个重要的优化 - 同步消除

如果你定义的类的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: