深入理解JVM之编译优化

JDK在源码编译阶段将源码编译为JVM字节码,JVM字节码是一种平台无关的中间代码方式,要由JVM在运行期间对其进行解释并执行,这种方式成为字节码解释执行方式。
对于面向对象的语言而言,最重要的是执行方法的指令,JVM有一套自己的执行方法的指令:invokestatic(调用static方法)、invokevirtual(调用对象实例的方法)、invokeinterface(调用接口的方法)、invokespecial(调用private方法和编译源码后生成的方法,此方法为对象实例化时的初始化方法)

字节码是在栈中执行
线程创建时,会产生程序计数器(PC)、栈,PC存放下一条要执行的指令在方法内的偏移量。栈中存放栈帧,栈帧主要分为局部变量区、操作数栈两部分。
局部变量区用于存放方法的局部变量和参数,操作数栈用于存放方法执行过程中产生的中间结果,栈帧中还有一些其它空间,如方法已解析的常量池引用。


深入理解JVM之编译优化_第1张图片
图片发自App
void foo(){
    int a = 1;
    int b = 2;
    int c = (a + b) * 5;
}

编译后字节码:

code:
0:iconst_1 //将类型为int、值为1的常量放入操作数栈
1: istore_0 //将操作数栈中栈顶的值弹出放入局部变量区
2:iconst_2 //将类型为int、值为2的常量放入操作数栈
3:istore_1 //将操作数栈中栈顶的值弹出放入局部变量区
4:iload_0 //装载局部变量区中第一个值到操作数栈
5:iload_1 //装载局部变量区中第二个值到操作数栈
6:iadd //执行int类型的add指令,并将计算结果放入操作数栈
7:iconst_5 //将类型为int、值为5的常量放入操作数栈
8:imul //执行int类型的mul指令,并将计算结果放入操作数栈
9:istore_2 //将操作数栈中栈顶的值弹出并放入局部变量区
10:return //返回

编译执行
解释执行的效率较低,为提升代码的执行性能,JVM提供将字节码编译为机器码的支持,编译在运行时进行,通常称为JIT编译器,JVM在执行过程中对执行频率高的代码进行编译,对执行不频繁的代码则继续采用解释执行的方式。
编译执行有两种模式:client compiler(-client)和server compiler(-server)
client compiler又称为C1,较轻量级,只做少量性能开销比较高的优化,它占用内存少,适合桌面交互式应用,它的优化方式主要有:方法内联,去虚拟化,冗余削除等。
1,方法内联:在方法中需要调用其它方法,需要经历参数传递、返回值传递及跳转等,方法内联即把调用到的方法的指令直接植入到当前方法中
2,去虚拟化:在装载class之后,进行类层次的分析,如发现接口的方法只提供一个实现类,那么对于调用了此方法的代码,也可以进行方法内联。
3,冗余削除:在编译时,根据运行时状况进行代码折叠或削除。去掉不需要的代码指令。
Server compiler又称为C2,较为重量级,C2采用大量传统编译优化技巧,占用内存多,适用于服务器端应用。
“逃逸分析”是C2进行很多优化的基础,逃逸分析是指根据运行状况来判断方法中的变量是否会被外部读取,如不会则认为此变量是逃逸的,基于逃逸分析C2在编译时会做标量替换、栈上分配、同步削除等
1,标量替换:用标量替换聚合量,见代码:

Point point = new Point(1,2);
System.out.println("point.x="+point.x+"; point.y="+point.y);

当point对象在后面的执行过程中未用到时,经过编译后,代码会变成类似下面的结构:

int x = 1;
int y = 2;
System.out.println("point.x="+x+"; point.y="+y);

这种方式的好处是,如果创建的对象并未用到其中的全部变量,则可节省一定的内存,对于代码执行而言,由于无需去找对象的引用,也会更快一些。
2,栈上分配:如果上例中,point是逃逸的,那么C2会选择在栈上直接创建point对象实例,而不是在JVM堆上,在栈上分配的好处一方面是快速,另方面是垃圾回收时随着方法的结束,对象也就被回收了。
3,同步削除:指同步的对象逃逸,方法外部没有引用到同步的对象,那就没有同步的必要了,C2编译时会直接去掉同步。

JVM会根据机器配置来选择C1还是C2,当机器配置CPU达到2核且内存超过2G则选择C2,但是32位windows机器上始终选择C1模式,也可在启动时通过-client或-server来强制指定。
基于这个特性,在对java代码进行性能测试时,要注意是否实现做了足够次数的调用,以保证测试是公平的。对于高性能的程序而言,也应考虑在程序提供给用户访问前,自行进行一定的调用,以保证关键功能的性能。

你可能感兴趣的:(深入理解JVM之编译优化)