1. Java中字节码、机器指令
字节码是指平常所了解的 .class 文件,通过 javac 命令编译成字节码。
机器指令是指机器可以直接识别运行的代码,字节码是不能直接运行的,JVM 通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译才能运行。很显然逐条的解释其执行速度必然会比可执行的二进制字节码程序慢很多,这是传统的JVM的解释器(Interpreter)的功能。为了提高执行速度,引入了 JIT 技术。
JIT:在运行时 JIT 会把翻译过的机器码保存起来,以备下次使用,从而提高热点代码的执行效率。
热点代码:某个方法或代码块运行频繁。
2. JIT概述
Java程序还是通过解释器进行解释执行,当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是热点代码。然后JIT会把部分“热点代码”翻译成本地机器相关的机器码,并进行优化,然后再把翻译后的机器码缓存起来,以备下次使用。
当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制(如部分嵌入式系统中),可以使用解释器执行节约内存,反之可以使用编译执行来提升效率。
3 客户模式或服务器模式
HotSpot虚拟机中内置了两个JIT编译器:Client Complier和Server Complier,分别用在客户端和服务端,目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作
JVM Server 模式与 client 模式启动,最主要的差别在于:-server 模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。原因是:当虚拟机运行在-client 模式的时候,使用的是一个代号为 C1 的轻量级编译器,而-server 模式启动的虚拟机采用相对重量级代号为 C2 的编译器。C2 比 C1 编译器编译的相对彻底,服务起来之后,性能更高。
通过 java -version 命令行可以直接查看当前系统使用的是 client 还是 server 模式。例如:
4 热点代码
JVM为每个方法分配一个调用计数器和为每个已执行的循环体配一个计数器(回边计数器)。如果一个具体方法的方法进入计数和循环边计数超过了由运行时设定的编译临界值,则认定它为性能关键的方法。运行时使用这些指标来判定这些方法本身或其调用者是否是性能关键的方法。
方法计数器:判断是否已存在编译版本,如已存在,则执行编译版本;否则,方法计数器+1,判断两个计数器之和(注意:是方法计数器和回边计数器的和)是否超过方法计数器阈值,超过则向编译器提交编译请求,这个方法将排队等待编译。不超过阈值情况仍旧解释执行该方法。
注意:该计数器并不是绝对次数,而是相对的执行次数,即在一段时间内的执行次数,当超过一定的时间限度,若还是没有达到阈值,那么它的计数器会减少一半,此过程被称为热度衰减。
注意:回边计数器:当两个计数器之和超过阈值的时候,它向编译器提交OSR编译,并且调整回边计数器值,然后仍旧以解释方式执行下去。
PS:该计数器是绝对次数,没有热度衰减。
编译的代码存在在一个称为“代码缓存”的缓存里。JIT除了具有缓存编译的代码功能外,还会对代码做各种优化,例如:逃逸分析、锁消除、锁膨胀、方法内联、数据边界检查、空值检查消除、类型检测消除、公共子表达式消除等。
5. 逃逸分析
逃逸分析的基本行为就是分析对象动态作用域。当一个对象在方法中被定义后,它可能被外部方法所引用。主要分为以下两类:
- 方法逃逸:当一个对象在方法里面被定义后,它可能被外部方法所引用
- 线程逃逸:当一个对象在方法里面被定义后,它可能被外部线程访问到
根据逃逸分析证明一个对象不会逃逸到方法或线程中,则进行高效的优化:
- JVM中,对象一般在堆中分配,堆是线程共享的,进行垃圾回收和整理内存都是消耗时间的。所有确定一个对象不会逃逸时,让对象从栈上分配内存可以缓解垃圾回收的压力。
- 如果确定一个变量不会逃逸出线程,则消除掉变量的同步措施,也成为锁消除。
- 标量替换或者分离对象:
标量是一个数据已经无法再分解成更小的数据,JVM中的原始数据类型(int、long等数值类型以及reference类型等)。反之为聚合量即Java对象。如果对象不会逃逸,则不创建该对象。方法执行时直接创建若干个相关的变量来替代。并且对象拆分后,对象的成员变量在栈上分配和读写,为进一步优化提供条件。
在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,
-XX:+DoEscapeAnalysis : 表示开启逃逸分析
-XX:-DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析
6. 方法内联
该方法是针对Client而言的,方法调用本身是有代价的,要从常量池找到方法地址,然后保存当前栈帧状态,压入新栈帧启动调用过程,调用完弹出,并恢复调用者栈帧。而在运行期,如果方法很频繁的执行,就会运行期把方法内联到调用者方法内部,减少频繁调用的开销。
看例子:
// 优化前
public static void foo(Object obj){
Sout("do something");
}
public static void test(String[] args){
Object obj = null;
foo(obj);
}
// 优化后
public static void test(String[] args){
Object obj = null;
Sout("do something");
}