是否是不少人的印象中,Java要比C++运行的慢?若是如今还停留在这个想法,那或许该更新下想法了,由于Java近几年在运行优化方面作了很是多的研究和改进,能够说已经基本不怎么输于C++的运行速度了。前端
咱们参照HotSpot虚拟机的优化来讲明,不一样虚拟机确定是不一样的,可是也有参考价值。java
个人全部文章同步更新与Github--Java-Notes,想了解JVM,HashMap源码分析,spring相关,剑指offer题解(Java版),能够点个star。能够看个人github主页,天天都在更新哟。git
邀请您跟我一同完成 repo程序员
所谓无风不起浪,**为啥你们以为Java的执行速度要比C++慢呢?**如今又怎么说两者如今的执行速度差异不太大了呢?github
由于早期的Java代码主要都是由解释器来完成,而且即便用到即时编译器,即时编译器的性能优化也作得不是太好,因此早期才有了上面说的"Java比C++执行慢",因此才说那是早期的思想。,如今Java优化作的很是好,咱们就挨个说下吧算法
如今主流的(不是所有)虚拟机都采用解释器和编译器共存的架构,两者互相配合工做,以下图所示spring
两者互有优缺点,优缺点正好相对。后端
因此啊,虚拟机才会配合两者进行使用。那些"热点代码"(这个概念后面会提到)会被即时编译器编译成本地代码,以此来提升执行速度,"非热点代码"就用解释器来执行。为啥这样呢?可能符合"二八定理"吧。20%的代码是热点代码,可是他们却可能占用80%的执行资源。数组
咱们看到上面的图片中,编译器有两种,一个是客户端的,一个是服务端的,前者被称为C1编译器,后者称为C2编译器,两个的参数设置也不同。优化手段也不太同样,后面会讲到。缓存
咱们以前提到,即时编译器只编译那些"热点代码",那么啥是热点代码呢?只有如下两类
前者比较好理解,后者有点很差理解。若是一个方法中,存在循环屡次的循环体,那么这个循环体的代码也被执行了不少次,因此也认为是"热点代码"。可是虽然是因为循环体形成他是热点代码的,可是编译器编译的时候,是根据整个方法进行编译(而不是只编译循环体)。这种编译方式因为编译发生在方法执行过程当中,所以被形象的称为栈上替换(On Stack Replacement,简称OSR)。前者也是以整个方法做为编译对象。
那么啥是度呢?多少次才叫屡次?才会触发即时编译条件呢?
判断一段代码是否是热点代码,是否是须要触发即时编译,咱们称之为热点探测,主流的热点探测断定方式有两种:
基于采样的热点探测:采用这种方法的虚拟机会周期性的检查各个线程的栈顶,若是发现某个(或者某些)方法常常出如今栈顶,那这个方法就是"热点方法"。(有没有像可达性分析法搜索可达路径?)
基于计数器的热点探测:为每个方法(甚至是代码块)创建一个计数器,统计是否超过阈值。还记得两种热点代码吗?他专门准备了两种相对的计数器。前者是方法调用计数器,后者是回边计数器
两个方法优劣对比:
基于采样
基于计数器
咱们先看图
当一个方法执行时,
这里的方法调用计数器里面的值不是绝对值,而是一个相对的执行频率,即一段时间以内方法被调用的次数。超过必定时间,调用次数仍然未达到阈值,那么方法调用计数器的值就会减小一半,这个过程称为方法调用计数器热度的衰减。这个像不像原子的衰变?而后这个时间,他们就取名为半衰周期。可是这个是能够关掉的,相关参数设置在参数那段讲
啥是回边呢?在字节码中遇到控制流向后跳转的指令,被称为"回边",这个计数器也是为了触发OSR编译(这个概念,上文讲过)
执行步骤和方法调用计数器类似
不一样的是,这个计数器没有所谓的半衰
-client/-server
:指定JVM运行哪一种模式-Xint
:强制JVM使用解释模式,编译器不工做-Xcomp
:和上面不一样的是,他只是优先编译器工做,当编译没法进行的状况下,解释器仍是会介入-XX: -UseCounterDecay
:关闭热度衰减-XX: CounterHalfLifeTime
:设置半衰期时长,单位秒-XX: CompileThreshold
:设置方法调用阈值-XX: BackEdgeThreshold
:设置回边计数器阈值-XX: -BackgroundCompilation
:禁止后台编译,达到阈值,等到编译完再往下执行,且执行编译好的代码一行长度为32bit
后台编译过程,Client Compiler (C1)和Server Compiler(C2)的工做是不同的,后者更为复杂,是全局优化,前者主要是局部性的优化
共分为三个阶段:
一共有很是多的技术
不过最具备表明性的是如下四个:
若是一个表达式E已经计算过了,而且先前的计算到如今E中全部变量的值都没有发生变化,那么E的此次出现就成为了公共子表达式
若是有一下程序代码
int d = (c * b) * 12 + a + (a + b *c);
当这段代码进入虚拟机即时编译器后,他将进行以下优化:
int d = E * 12 + a + (a+E)
int d =E * 13 + a *2
咱们知道,相同的代码,Java要比C++执行的慢,这也是人们会有一开始的那种想法的缘由之一。那么为何呢?
Java程序因为有虚拟机,不少东西都会替你来作,好比越界检查,除零检查,垃圾回收这种。
好比数组 nums[]
,Java中若是你要访问其中一个元素,你的下标必定是在[0,num.length-1]的,若是超出了,他就会报错java.lang.ArrayIndexOutOfBoundsException
.可是C或者C++,使用的是裸指针,所以这种判断须要程序员本身操做。
Java中,若是你要对数组进行读写操做,那么就必定要检查他的范围是否超出,这是由JVM进行,可是大量的执行步骤中,都会带有这个,哪怕你知道不可能超出,可是JVM可不知道。
因此能不能告诉他,"咱们的必定不会超出,你别再检查了呢?"
事实上,不行,,由于数组越界检查是必须作的。可是能够跟他谈判,让他减小次数,提早到编译期检查一次
好比咱们有下面的程序 nums[3]
,只要在编译期间根据数据流分析来肯定nums.length
的值,而且判断下标 3并无越界,那么执行的时候就不必判断了。
用于:
若是有下列代码
if(num != null){
return num.getVal();
}else {
throw new NullPointerException();
}
复制代码
使用隐式异常优化后
try{
return num.val;
}catch(segment_fault){
uncommon_trap();
}
复制代码
虚拟机会注册一个Segment Fault
信号的异常处理器,
NullPointerException
异常
他的重要意义是为其余优化手段打下基础,为啥这样说呢,请看下面的示例:
public class DeadCode {
public static void testInline(String[] args) {
Object object = null;
foo(object);
}
public static void foo(Object object){
if(object != null){
System.out.println("do something");
}
}
}
复制代码
咱们本身看下就应该知道,testInline()
方法中的代码根本没有一点意义。咱们称这种代码是"Dead Code"
若是不进行内联,虚拟机根本不会发现,即便他进行了无用代码消除,他在即时编译的时候也不会消除这些代码。由于分开来看,这两个方法都是有意义的啊
方法内联的行为看起来彷佛很简单
可是真的这么简单吗?不是的,根据经典编译原理的优化理论,大部分的Java方法都不能够内联
咱们知道方法的调用分为:解析和分派,尤为是分派中的动态分派,编译期是不能知道调用的是哪一个类的方法(能够参考个人这篇文章,重载和重写的区别-方法调用层次)
好比下面的代码
public class DeadCode {
static class ParentB{
int val;
public int getVal(){
return val;
}
public ParentB(int val){
this.val = val;
}
public ParentB(){
}
}
static class SubB extends ParentB{
public int getVal(){
return val;
}
public SubB(int val){
this.val = val;
}
public SubB() {
}
}
public static void main(String[] args) {
ParentB subB = new SubB(5);
System.out.println(subB.getVal());
}
}
复制代码
编译期,他是不会知道 getVal()
调用的是 sub的仍是Parent的方法,只有运行期才会知道
那怎么办呢?
首先JVM团队想到CHA(类型继承关系分析,Class Hierarchy Analysis)技术。
一种基于整个应用程序的类型肥西技术,用于肯定在目前已加载的类中,某个接口是否有多于一种的实现,某各种是否存在子类、子类是否为抽象类等信息。
编译器在内联时,若是是非虚方法(若是不知道这个概念,能够参考个人这篇方法调用的文章,重载和重写的区别-方法调用层次),那么直接内联就行
若是遇到虚方法,则会向CHA查询此方法在当前程序下是否有多个目标版本可供选择,
若是向CHA查询的结果是有多个版本的目标方法能够选择,
编译器会进行最后一次尝试,使用内联缓存完成内联
这个是创建在目标方法正常入口以前的缓存
未发生方法调用以前,内联缓存为空
发生第一次调用,缓存记录下方法接收者的版本信息,而且每次调用都比较接收者版本
他和上面的CHA(类型关系继承分析)同样,都不是直接优化的手段,而是为其余优化手段提供依据的分析技术
逃逸分析的基本行为就是:分析对象动态做用域
若是能证实一个对象不会有方法逃逸和线程逃逸,那就能够对其进行高效的优化: