通过类装载器装载的,被分配到JVM的运行时数据区的字节码会被执行引擎执行。执行引擎以指令为单位读取Java字节码。它就像一个CPU一样,一条一条地执行机器指令。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。
不过Java字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎必须把字节码转换成可以直接被JVM执行的语言。字节码可以通过两种方式转换成合适的语言。
1、解释器
一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。
2、JIT编译器
即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。
不过,用JIT编译器来编译代码所花的时间要比用解释器去一条条解释执行花的时间要多。因此,如果代码只被执行一次的话,那么最好还是解释执行而不是编译后再执行。因此,内置了JIT编译器的JVM都会检查方法的执行频率,如果一个方法的执行频率超过一个特定的值的话,那么这个方法就会被编译成本地代码。
当 JVM 执行代码时,它并不立即开始编译代码。这主要有两个原因:
首先,如果这段代码本身在将来只会被执行一次,那么从本质上看,编译就是在浪费精力。因为将代码翻译成 java 字节码相对于编译这段代码并执行代码来说,要快很多。
当 然,如果一段代码频繁的调用方法,或是一个循环,也就是这段代码被多次执行,那么编译就非常值得了。因此,编译器具有的这种权衡能力会首先执行解释后的代 码,然后再去分辨哪些方法会被频繁调用来保证其本身的编译。其实说简单点,就是 JIT 在起作用,我们知道,对于 Java 代码,刚开始都是被编译器编译成字节码文件,然后字节码文件会被交由 JVM 解释执行,所以可以说 Java 本身是一种半编译半解释执行的语言。Hot Spot VM 采用了 JIT compile 技术,将运行频率很高的字节码直接编译为机器指令执行以提高性能,所以当字节码被 JIT 编译为机器码的时候,要说它是编译执行的也可以。也就是说,运行时,部分代码可能由 JIT 翻译为目标机器指令(以 method 为翻译单位,还会保存起来,第二次执行就不用翻译了)直接执行。
第二个原因是最优化,当 JVM 执行某一方法或遍历循环的次数越多,就会更加了解代码结构,那么 JVM 在编译代码的时候就做出相应的优化。
主流的JVM中,Java程序最初是通过解释器(Interpreter)进行解释执行的。当JVM发现某个方法或代码块运行特别频繁时,就会认为这是“热点代码”(Hot Spot Code)。
为了提高热点代码的执行效率,就会将这些“热点代码”编译成与本地机器相关的机器码,进行各个层次的优化。完成这个任务的编译器就是即时编译器(JIT)。
JIT编译器是“动态编译器”的一种,相对的“静态编译器”则是指的比如:C/C++的编译器。
JIT并不是JVM的必须部分,JVM规范并没有规定JIT必须存在,更没有限定和指导JIT。但是,JIT性能的好坏、代码优化程度的高低却是衡量一款JVM是否优秀的最关键指标之一,也是虚拟机中最核心且最能体现虚拟机技术水平的部分。
热点代码分为2类:
热点判定方式,两种:
1、基于采样的方式探测(Sample Based Hot Spot Detection) 。周期性检测各个线程的栈顶,发现某个方法经常出险在栈顶,就认为是热点方法。好处就是简单,缺点就是无法精确确认一个方法的热度。容易受线程阻塞或别的原因干扰热点探测。
2、基于计数器的热点探测(Counter Based Hot Spot Detection)。某个方法超过阀值就认为是热点方法,触发JIT编译。(涉及计数器的热度半衰减过程)
两个计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)
触发了JIT编译,编译工作完成,这个方法的入口就会被系统自动改为新的编译入口,就会调用编译的版本。
JVM中默认内置了两款即时编译器,称为Client Compiler和Server Compiler。可以用指定参数的方式,指定采用Client模式和Server模式,默认是mixed模式。
java -Xint 解析 java -Xcomp 编译
Client Compiler和Server Compiler会实现分层编译(JDK1.7默认有)。
第0层 程序解析执行,解析器不开启性能监控,可触发第一层编译;
第1层 编译成本地相关代码,进行简单优化;
第2层 除编译成本地相关代码外,还进行成编译耗时较长的优化。
一般认为,编译器的本地会比javac的产生的字节码更优秀。
常见的优化技术很多:例如公共表达式的消除,数组边界检查的消除,方法的内联(最重要的优化技术)
逃逸分析:当一个对象被定义后,可能被外部方法引用,例如被当作参数传递到其他方法中,称为方法逃逸。可以被其他线程访问,这个称为线程逃逸。
若能证明这个对象不会逃逸到其他方法或线程中,就可以进行高效的优化。
1、栈上分配 在堆上分配对象内存,回收整理内存需要消耗时间,若在栈上分配内存将是个不错的主意。被对象占用的空间就可以随帧栈的就出栈而销毁。大量的对象随方法的结束而自动销毁,GC也减轻压力。
2、同步消除 若不会线程逃逸,不会有竞争,方法上的同步措施就会消除。
3、标量替换 JAVA的原始类型无法再分就是一个标量,对象就是聚合量。对象若可以被拆分成标量,直接在栈上分配,就是类似栈上分配内存,甚至分配到高速缓存中。
逃逸分析不成熟的原因:不能保证逃逸分析的性能收益大于它的消耗。
Java与C/C++编译器的对比,代表了最经典的即时编译器与静态编译器的对比。除了自身API的实现的好坏,更多是一场“拼编译器”和“拼输出代码质量”的游戏。
两种语言的编译器的优劣:
1、即时编译器运行占用的是用户的运行时间,具有很大的时间压力,它提供的优化手段严重受制与编译的成本。而编译的时间在静态编译中并不是主要关注点。
2、java语言是动态的类型安全语言,意味着由虚拟机确保程序不会违反语言的语义或访问非结构化内存。虚拟机需要频繁的动态检查,如空指针,数组越界,继承关系等,总体消耗不少时间。
3、java语言使用虚方法的频率远大于C/C++,意味对方法的接收者进行多态的选择频率远大于C/C++,意味着即时编译器的优化难度远远大于C/C++编译器。
4、java语言是动态扩展的语言,运行时会加载新的类,改变程序的继承关系,使得很多全局优化难以进行。只能采用激进的方式,在运行时撤销或重新进行一些优化。
5、java语言的对象是在堆上分配,只有方法的局部变量才在栈上分配。而C/C++语言有多种分配方式,既可以在堆上分配,又可以在栈上分配,减轻了内存回收的压力。另外C/C++语言主要由用户代码回收内存,不存在无用对象筛选的过程,效率要比垃圾回收机制要高。
Java语言在性能上的劣势都是为了换取开发效率上的优势而付出的代价。动态安全,动态扩展,垃圾回收这些“拖后腿”的特性都是为JAVA的开发效率做出了很大的贡献。
由于C/C++的静态编译,以运行性能监控为基础的优化措施它都无法进行,如调用频率预测,分支频率预测,裁剪未使用分支等,这些都是称为java语言独有的性能优势。