Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或者代码块运行特别频繁时,就会把这些代码认定为热点代码,为了提高热点代码的执行效率,运行时虚拟机将会把热点代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器。即时编译生成机器相关的中间码,可重复执行缓存效率高。解释执行直接执行字节码,重复执行需要重复解释。
即时编译(JIT)与预编译(AOT):在Dalvik下,应用每次运行都需要通过即时编译器(JIT)将字节码转换为机器码,即每次都要编译加运行,这虽然会使安装过程比较快,但是会拖慢应用的运行效率。而在ART 环境中,应用在第一次安装的时候,字节码就会预编译(AOT)成机器码,这样的话,虽然设备和应用的首次启动(安装慢了)会变慢,但是以后每次每次打开应用,执行的都是本地机器码。移除了运行时的解释执行都可以直接运行,因此运行效率会提高。Android7.0版本ART加入了即时编译器JIT,作为AOT的一个补充,在应用程序安装时并不会将字节码全部编译成机器码,在运行中将热点代码编译成机器码,从而缩短应用程序的安装时间并节省存储空间。
解释执行:将编译好的字节码一行一行地翻译为机器码执行。编译执行:以方法为单位,将字节码一次性翻译为机器码后执行。当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行;当程序运行后,随着时间的推移,编译器逐渐会失去作用,把越来越多的代码编译成本地代码后,可以获取更高的执行效率。解释执行可以节约内存,而编译执行可以提升效率。目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。
编译对象与触发条件
运行过程中会被即时编译器编译的热点代有两类:
(1)被多次调用的方法。
(2)被多次调用的循环体。
以上两种情况,编译器都是以整个方法作为编译对象,这种编译也是虚拟机中标准的编译方式。一段代码或方法是不是热点代码,是不是需要触发即时编译,需要进行Hot Spot Detection(热点探测)。
目前主要的热点 判定方式有以下两种:
(1)基于采样的热点探测:虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这段方法代码就是“热点代码”。好处是:实现简单高效,还可以很容易地获取方法调用关系;缺点是:很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
(2)基于计数器的热点探测:虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值,就认为它是“热点方法”。这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对更加精确严谨。
在HotSpot虚拟机中使用的是第二种——基于计数器的热点探测方法的两个计数器:
(1)方法调用计数器:用来统计方法调用的次数,在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。
(2)回边计数器:用于统计一个方法中循环体代码执行的次数.
(准确地说,应该是回边的次数,因为并非所有的循环都是回边),在字节码中遇到控制流向后跳转的指令就称为“回边”。
在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阀值,当计数器的值超过了阀值,就会触发JIT编译。
即时编译和解释执行的执行顺序:发了JIT编译后,在默认设置下,执行引擎并不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成为止(编译工作在后台线程中进行)。当编译工作完成后,下一次调用该方法或代码时,就会使用已编译的版本。方法调用计数器触发即时编译的流程:方法计数器触发即时编译的过程与回边计数器触发即时编译的过程类似。方法调用计数器触发即时编译流程如下图所示:
编译优化技术
以编译方式执行本地代码比解释方式更快,除去虚拟机执行字节码时额外消耗时间的原因外,还有一个重要原因,虚拟机设计团队几乎把对代码的所有优化措施都集中在了即时编译器之中,代码优化变换是建立在代码的某种中间标志或者机器码之上,绝不是建立在java源码之上的。华为方舟编译器直接将代码优化从手机环节搬到了开发者环境,可以在很多地方对代码进行优化,同时即时编译器本地代码比javac产生的的字节码更优秀。
即时编译器针对不同的类型有不同的优化技术,具体可以参考《深入理解JVM》,几种比较有代表性的优化技术有:
(1)语言无关的经典优化技术之一:公共子表达式消除
int d = (C*B)*12 +a + (A+B*C)
int d = (E)*12 + a +A + E;
int d = 13E +2A;
以上就是公共子表达式消除;
(2)语言相关的经典优化技术之一:数组范围检测消除:数组边界检查肯定是必须做的,但是数组边界检查不是必须在运行期间一次不漏的检查,如果通过数据流分析可以判断循环变量的取值永远在数组边界之内,就可以在循环中将数组上下界检测消除。
(3)最重要的优化技术之一:方法内联:方法内联就是消除一些无用的代码,如方法一已经进行了null判断,如果调用方法二又进行了null判断,实际两次null判断仅需要一次。
(4)最前沿的优化技术之一:逃逸分析:如果一个对象不会逃逸到方法或者线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高校的优化,例如本来是堆上分配的变量发现没有其他线程使用,可以将其分配到栈上,栈上分配的对象就会随着方法的结束而自动销毁。
(5)同步消除(synchronized):类似于锁优化,变量不会逃逸到出方法,就可以将该方法上的同步锁去掉,相关内容请参考《深入理解JVM》线程安全与锁优化章节。