早期(编译期)优化:
javac把java文件转变成class文件,这类前段编译器堆代码的运行效率几乎没有任何优化措施,性能的优化集中到了后端的即时编译器中,但是javac针对java语言编码过程的优化措施来提高编码效率。
javac编译过程:
java语法糖,在语义分析与字节码生成阶段。
- 泛型与类型擦除
本质是参数化类型的应用,也就是操作的数据类型被指定为一个参数。可以应用到类、接口、方法上。泛型其实是javac提供给我们的一颗语法糖,因为它在编译阶段采用类型擦除,将泛型还原为裸类型。然后在适当的位置加入类型转换操作。例如:ArrayList list在编译后,我们再反编译class文件,可以看到代码变成了ArrayList list。这是一种伪泛型。在c#中,List和List是完全不同的两个类型,是真实泛型,而在java中由于类型擦除,他们是相同的类型。所以,一个类中如果声明了两个方法void fun(List) 和 void fun(List)是不能通过编译的,很显然,他们被类型擦出后,变成了相同参数类型。如果改成void fun(List) 和 int fun(List),就可以编译过了(JDK1.6以后)!返回值类型不是不参与重载么?价值观被颠覆了?其实返回值类型并没有参与重载,但是在Class文件格式中,只要描述符不是完全相同的方法就可以共存。后来为了获取参数化类型,虚拟机规范做了修改(JDK1.5),引入了Signature等解决泛型带来的参数类型识别问题。Signature就保存了参数化类型的信息。 - 自动装箱、拆箱与遍历循环
- 条件编译
晚期(运行期)优化:
java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行特别频繁时,就把这些代码认定为“热点代码”,为了提高热点代码的执行效率,运行时,虚拟机把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器成为即时编译器(JIT编译器)。
Java虚拟机规范并没有规定虚拟机内必须要有即时编译器。但是,即时编译器性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一,它也是虚拟机中最核心最能体现技术水平的部分。
hotspot虚拟机内的即时编译器:
hotspot和j9都是解释器和编译器并存,保留解释器的原因是,加快启动时间,立即执行,当运行环境中内存资源限制较大时,解释器可以节约内存,解释器还可以作为激进优化的编译器的“逃生门”(称为逆优化Deoptimization),而编译器能把越来越多的代码编译成本地代码后,获取更高的执行效率。
hotspot内置了两个即时编译器,clientcompiler和servercompiler,称为C1,C2(clientcompiler获取更高的编译速度,servercompiler来获取更好的编译质量),默认是采用解释器与其中一个编译器直接配合的方式工作。HOTSPOT会根据自身版本和宿主机器的性能自动选择运行模式,用户也可以使用-client或-server来决。这种解释器编译器搭配的方式成为混合模式,用户还可以使用-Xint强制虚拟机使用“解释模式”,也可以使用-Xcomp强制“编译模式”。
被编译的触发条件:
1. 被多次调用的方法
2. 被多次执行的循环体(栈上替换)OSR On StackReplacement
判断是否是热点代码的行为成为热点探测:hot spotdetection,主要的热点探测方式主要有两种:
1. 基于采样的热点探测,JVM会周期性检查各个线程的栈顶,如果某个方法经常出现在栈顶,那就认定为热点方法。简单高效,精度不够。
2. 基于计数器的热点探测,统计方法执行次数。(hotspot使用这种方式)
编译优化技术:
JDK设计团队几乎把代码的所有优化措施都集中在了即使编译器,所以一般来说即即时编译器产生的本地代码会比Javac产生的字节码更优秀。接下来介绍几种景点优化技术:
1.公共子表达式消除:如果一个表达式之前已经计算过了,并且参与者期间都没有发生变化,那该表达式就是公共子表达式,不需要再次计算。
2.数组边界检查消除:java是动态安全的,访问数组前会先判断下表是否越界,但每次运行都判断,未免浪费效率。编译器在编译期间如果确定不会越界,就省略判断,运行时就可以提高效率。还有一种思路,是不判断,而是等出异常再处理,对于大多数情况正常的代码,能提升效率。
3.方法内联:不只是消除了调用的消耗,主要是为其他优化提供了基础。
4.逃逸分析:如果能证明一个对象不会逃逸到方法或者线程外,就可以做很多优化。
a.栈上分配:对象所占用的内存空间可以随栈帧出栈而销毁,若在堆里分配的话,回收和整理内存都需要消耗时间
b.同步消除:线程同步本身就是一个相对耗时的过程,若确定不会逃逸出线程,对这个变量就不需要实施同步措施
c.标量替换:不能再拆分的基本类型就是标量,对象就是聚合量。如果一个对象不会被外部访问,那将可能不创建对象,而是用组成他的一些标量的集合代替它。拆分后,不仅可以让标量在栈上,还可以为后续优化做基础。