首先咱们先来了解一下运行在虚拟机之上的解释器与JIT编译器。java
当咱们的虚拟机在运行一个java程序的时候,它能够采用两种方式来运行这个java程序:c++
这两种方式能够说是各有优点,虚拟机(特指HotSpot虚拟机)在执行的时候,通常会采用两种方式结合的策略。算法
也就是说,在程序执行的时候,有些代码采用解释器的方式,有些代码采用编译器,称之为即时编译。通常咱们会对热点代码采用编译器的方式。编程
上面已经说了,运行过程当中,若是遇到热点代码就会触发对该代码进行编译,编译成本地机器码。数组
什么是热点代码?安全
热点代码主要有一下两类:微信
不过这里须要注意的是,因为循环体是存在方法之中的,尽管编译动做是由循环体触发的,但编译器仍然会以这个方法来做为编译的对象。性能
判断一段代码是否是热点代码,是否是须要触发即时编译,这样的行为咱们称之为热点探测。热点探测断定有如下两种方式:优化
两种方法的优缺点:线程
显然第一种方法在实现上是比较简单、高效的,可是缺点也很明显,精确度不高,容易受到线程阻塞等别的外界因素的干扰。
第二种方式的统计结果会很精确,但须要为每一个方法创建并维护一个计数器。实现上会相对复杂一点而且开销也会大点。
不过,这里须要指出的是,咱们的HotSpot虚拟机采用的是基于计数器的方式。
说明:虚拟机在执行方法的时候,会先判断该方法是否存在已经编译好的版本,若是存在,则执行编译好的 本地机器码,不然,采用一边解释一边编译的方式。
先看一段代码:
int a = 1; if(false){ System.out.println("无用代码"); } int b = 2;
对于这段代码,咱们都知道是if语句体里面的代码是必定不可能会被执行到的,也就是说,这其实是一段一点用处也没有的代码,在执行时只能浪费判断时间。
实际上,对于咱们书写的代码,编译器在编译的时候是会进行优化的。对于上面的代码,编译优化以后会变成这样:
int a = 1; int b = 2;
那段无用的代码会被消除掉。
咱们刚才已经说了,对于有些被屡次调用的方法或者循环体,虚拟机会先把他们编译成本地机器码。因为这些热点代码都是一些会被屡次重复执行的代码,为了使得编译好的代码更加完美,运行的更快。编译器作了不少的编译优化策略,例如上面的无用代码消除就是其中的一种。
下面咱们来说讲大概都有那些优化策略:
大概预览一波:
(1).公共子表达式消除
含义:若是一个表达式 E 已经计算过了,而且从先前的计算到如今 E 中的全部变量的值都没有发生变化,那个 E 的此次出现就成为了公共子表达式。对于这样的表示式,没有必要对它再次进行计算了,直接沿用以前的结果就能够了。
咱们来举个例子。例如
int d = (c * b) * 10 + a + (a + b * c);
这段代码到了即时编译器的手里,它会进行以下优化:
表达式中有两个 b * c的表达式,而且在计算期间b与c的值并不会变。因此这条表达式可能会被视为:
int d = E * 10 + a+ (a + E);
接着继续优化成
int d = E * 11 + a + a;
接着
int d = E * 11 + 2a;
这样,代码在执行的时候,就会节省了一些时间了。
(2).数组范围检查消除
咱们知道,java是一门动态安全的语言,对数组的访问不像c/c++那样,能够采用指针指向一块可能不存在的区域。例如假若有一个数组arr[],在java语言中访问数组arr[i]的时候,是会先进行上下界范围检查的,即先检查i是否知足i >= 0 && i < arr.length这个条件。若是不知足则会抛出相应的异常。这种安全检查策略能够避免溢出。但每次数组访问都会进行这样一次检查无疑在速度性能上形成必定的影响。
实际上,对于这样一种状况,编译器也是能够帮助咱们作出相应的优化的。例如对于数组的下标是一个常量的,如arr[2],只要在编译期根据数据流分析来肯定arr.length的值,并判断下标‘2’并无越界,这样在执行的时候就无需在判断了。
更常见的状况是数组访问发生在循环体中,而且使用循环变量来进行数组的访问,对于这样的状况,只要编译器经过数据流就能够判断循环变量的取值范围是否在[0, arr.length)以内,若是是,那么整个循环中就能够节省不少次数组边界检测判断的操劳了。
对于这些安全检查所消耗的时间,实际上,咱们还能够采用另一种策略--隐式异常处理。例如当咱们在访问一个对象arr的属性arr.value的时候,没有优化以前虚拟机是这样处理的:
if(arr != null){ return arr.value; }else{ throw new NollPointException(); }
采用优化策略以后编程这样子:
try{ return arr.value; }catch(segment_fault){ uncommon_trap(); }
就是说,虚拟机会注册一个Segment Fault信号的异常处理器(uncommon_trap()),这样当arr不为空的时候,对value的访问能够省去对arr的判断。代价就是当arr为空时,必须转入到异常处理器中恢复并抛出NullPointException异常,这个过程会从用户态转到内核态中处理,结束后在回到用户态,速度远比一次判断空检查慢。当arr极少为null的时候,这样作是值得的,但假如arr常常为null时,那么会得不偿失。
不过,虚拟机仍是挺聪明的,它会根据运行期收集到的信息来自动选择最优方案。
(3).方法内联
先看一段代码
public static void f(Object obj){ if)(obj != null){ System.out.println("do something"); } } public static void test(String[] args){ Object obj = null; f(obj); }
对于这段代码,若是把两个方法结合在一块儿看,咱们能够发现test()方法里面都是一些无用的代码。由于f(obj)这个方法的调用,没啥卵用。可是若是不作内联优化,后续尽管进行了无用代码的消除,也是没法发现任何无用代码的,由于若是把f(Object obj)和test(String[] args)两个发放分开看的话,咱们就没法得只f(obj)是否有用了。
内联优化后的代码能够是这样:
public static void f(Object obj){ if)(obj != null){ System.out.println("do something"); } } public static void test(String[] args){ Object obj = null; //该方法直接不执行了 }
(4).逃逸分析
逃逸分析是目前Java虚拟机比较前沿的优化技术,它并不是是直接优化代码,而是为其余优化手段提供依据发分析技术。
逃逸分析主要是对对象动态做用域进行分析:当一个对象在某个方法被定义后,它有可能被外部的其余方法所引用,例如做为参数传递给其余方法,称之为方法逃逸,也有可能被外部线程访问到,例如类变量,称之为线程逃逸。
假如咱们能够证实一个对象并不会发生逃逸的话,咱们就能够经过一些方式对这个变量进行一些高效的优化了。以下所示:
1).栈上分配
咱们都知道一个对象建立以后是放在堆上的,这个对象能够被其余线程所共享,而且咱们知道在堆上的对象若是再也不使用时,虚拟机的垃圾收集系统就会对它进行帅选并回收。但不管是回收仍是帅选,都是须要花费时间的。
可是假如咱们知道这个对象不会逃逸的话,咱们就能够直接在栈上对这个对象进行内存分配了,这样,这个对象所占用的内存空间就能够随进栈和出栈而自动被销毁了。这样,垃圾收集系统就能够省了不少帅选、销毁的时间了。
2).同步消除
线程同步自己是一个相对耗时的过程,若是咱们能判断这个变量不会逃出线程的话,那么咱们就能够对这个变量的同步措施进行消除了。
3).标量替换
什么是标量?
当一个数据没法分解成更小的时候,咱们称之为变量,例如像int,long,char等基本数据类型。相对地,若是一个变量能够分解成更小的,咱们称之为聚合量,例如Java中的对象。
假如这个对象不会发生逃逸。
咱们能够根据程序访问的状况,若是一个方法只是用到一个对象里面的若干个属性,咱们在真正执行这个方法的时候,咱们能够不建立这个对象,而是直接建立它那几个被使用到的变量来代替。这样,不只能够节省内存以及时间,并且这些变量能够随出栈入栈而销毁。
不过,对于编译器优化的技术还有不少,上面这几种算是比较典型的。
本次讲解到这里。
完
参考书籍:深刻Java虚拟机
若是你习惯在微信公众号看技术文章
想要获取更多资源的同窗
欢迎关注个人公众号: 苦逼的码农 每周不定时更新文章,同时更新本身算法刷题记录。