一早期(编译期)优化
1概述
Java语言的“编译期”是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一些)把*.java文件转变成*.class文件的过程;也可能是指虚拟机的后端运行期编译器(JIT编译器,just in time compiler)把字节码转变成机器码的过程;还可能是指使用静态提前编译器(AOT编译器,ahead of time compiler)直接把*.java文件编译成本地机器代码的过程。下面列举了这三类编译过程中一些比较有代表性的编译器: Ø 前端编译器:sun的javac、eclipse JDT中的增量式编译器(ECJ)。
Ø JIT编译器:HotSpot VM 的C1、C2编译器。
Ø AOT编译器:GNU Compiler for the java、Excelsior JET。
这三类过程中最符合大家对java程序编译认知的应该是第一类,在后面的讲解里,提到的“编译期”和“编译器”都仅限于第一类编译过程。限制了编译范围后,对于“优化”二字的定义就需要宽松一些,因为javac这类编译器对代码的运行效率几乎没有任何优化措施(在JDK1.3之后,javac的-O优化参数就不再有意义了)。虚拟机设计团队把对性能的优化集中到了后端的即时编译器中,这样可以让那些不是由javac产生的class文件也同样能享受到编译器优化带来的好处。
但是javac做了许多针对编码过程的优化措施来改善程序员的编码风格和提高编码效率。相当多新生的java语法特性,都是靠编译器的“语法糖”来实现,而不是依赖虚拟机的底层改进来支持,可以说,java中即时编译器在运行期的优化过程对于程序运行来说更重要,而前端编译器在编译期的优化过程对于程序编码来说关系更加密切。
2.javac 编译器
分析源码是了解一项技术实现内幕的最有效的手段,javac编译器不像HotSpot虚拟机那样使用c++语言(包含少量C语言)实现,它本身就是一个由java语言编写的程序,这为纯java的程序员了解它的编译过程带来了很大的便利。
2.1 javac 的源码与调试
虚拟机规范严格定义了Class文件的格式,但是对如何把java源码文件转变为class文件的编译过程未作任何定义,所以这部分内容是与具体JDK实现相关的。从sun javac的代码来看,编译过程大致可以分为三个过程,分别是:
Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类,上述三个过程的代码逻辑集中在这个类的compile()和compile2()方法里。整个编译最关键的处理是由8个方法来完成的,分别是:
[javascript] view plaincopy
1. initProcessAnnotations(processors);//准备过程:初始化插入式注解处理器
2. delegateCompiler =
3. processAnootations( //过程2:执行注解处理
4. enterTrees(stopIfError(CompileState.PARSE, //过程1.2:输入到符号表
5. parseFiles(sourceFileObject))), //过程1.1:词法分析、语法分析
6. classnames);
7.
8. delegateCompiler.compile2(); //过程3:分析及字节码生成
9. case BY_TODO:
10. while(! todo.isEmpty())
11. generate(desugar(flow(attribute(todo.remove()))));
12. break;
13. //generate,过程3.4:生成字节码
14. //desugar,过程3.3解语法糖
15. //flow,过程3.2:数据流分析
16. //attribute,过程3.1:标注
语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机科学家Peter J. Landin发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
Java在现代编程语言之中属于“低糖语言”(相对于C#及许多其他jvm语言来说),尤其是JDK1.5之前的版本,“低糖”语法也是java语言被怀疑已经“落后”的一个表面理由。Java中最常用的语法糖主要是泛型、变长参数、自动装箱拆箱,等等,虚拟机运行时不支持这些语法,它们在编译阶段被还原回简单的基础语法结构,这个过程就被称为解语法糖。
1.
class="java">public static void main(String[] args){2. Map
map = new HashMap (); 3. map.put("hello", "你好");
4. map.put("how are you", "吃了没");
5. System.out.println(map.get("hello"));
6. System.out.println(map.get("how are you"));
7. }
1. public static void main(String[] args){
2. Map map = new HashMap();
3. map.put("hello", "你好");
4. map.put("how are you", "吃了没");
5. System.out.println((String)map.get("hello"));
6. System.out.println((String)map.get("how are you"));
7. }
1. public class GenericTypes {
2.
3. public static void method(List
4. System.out.println("invoke method(List
5. }
6.
7. public static void method(List
8. System.out.println("invoke method(List
9. }
10. }
1. public class GenericTypes {
2.
3. public static String method(List
4. System.out.println("invoke method(List
5. return "";
6. }
7.
8. public static int method(List
9. System.out.println("invoke method(List
10. }
11. return "1";
12. }
public static void main(String[] args){
1. List
2.
3. int sum = 0;
4. for(int i:list){
5. sum += i;
6. }
7. System.out.println(sum);
8. }
9. //上述代码编译之后的变化:
10. public static void main(String[] args){
11. List list = Arrays.asList(new Integer[]{
12. Integer.valueOf(1),
13. Integer.valueOf(2),
14. Integer.valueOf(3),
15. Integer.valueOf(4),
16. });
17.
18. int sum = 0;
19. for(Iterator localIterator = list.iterator(); localIterator.hasNext();){
20. int i = ((Integer)localIterator.next()).intValue();
21. sum += i;
22. }
23. System.out.println(sum);
24. }
1. public static void main(String[] args){
2. Integer a = 1;
3. Integer b = 2;
4. Integer c = 3;
5. Integer d = 4;
6. Integer e = 321;
7. Integer f = 321;
8. Long g = 3L;
9.
10. System.out.println(c == d); //false 值不等
11. System.out.println(e == f); //false 堆位置不同
12. System.out.println(c == (a + b)); //true "+"运算符拆包
13. System.out.println(c.equals(a + b));//true equals 值比较
14. System.out.println(g == (a+b));//true "+"运算符拆包
15. System.out.println(g.equals(a+b));//false 类型不同
16. }
1.
2. public static void main(String[] args){
3. if(true){
4. System.out.println("block 1");
5. }else{
6. System.out.println("block 2");
7. }
8. }
9. //此代码编译后class文件的反编译结果:
10. public static void main(String[] args){
11. System.out.println("block 1");
12. }
1. public static void main(String[] args){
2. while(false){
3. System.out.print("");
4. }
JAVA最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”而将它们编译成本地机器码,并进行各种层次的优化,完成这个任务的编译器成为即时编译器(Just In Time Compiler)。
Java虚拟机规范并没有规定虚拟机内必须要有即时编译器。但是,即时编译器性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一,它也是虚拟机中最核心最能体现技术水平的部分。
2.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使用这种方式)
HOTSPOT有两个计数器:方法调用计数器和回边计数器
方法调用计数器client默认1500次,server默认10000次,可以通过参数-XX:CompileThreshold来设定。调用方法时,会先判断是否存在编译过的版本,如果有则调用该版本,否则计数器加1,然后看方法调用计数器和回边计数器之和是否超过方法调用计数器的阈值。超过,则提交编译请求
方法调用计数器并不是统计方法调用绝对次数,而是一个相对执行频率,超过一定时间,如果方法调用次数不足以让它提交给编译器,则计数器就会被减少一半,这种现象称为热度衰减(Counter Decay),进行热度衰减的动作是在垃圾回收时顺便进行的,而这段时间就被称为半衰周期(Counter Half Life Time)可用-XX:-UseCounterDecay来关闭热度衰减,用-XX:CounterHalfLifeTime来设置半衰时间。
回边计数器用于统计方法中循环体的执行次数。字节码遇到控制流向后跳转 的指令成为回边。建立回边计数器统计的目的就是为了触发OSR编译。回边的控制参数有:
-XX:BackEdgeThreshold,-XX:OnStackReplacePercentage。
1.在Client模式下,回边计数器阀值计算公式:方法调用计数器阀值乘以OSR比率,然后除以100.
2.在server模式下,回边计数器阀值计算公式:方法调用计数器阀值乘以(OSR比率,然后减去解释器监控比率的差值)除以100。
与方法计数器不同,回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。
编译过程
对Client Compiler而言,是一个简单快速的三段式编译器,主要关注点在于局部性的优化,放弃了许多耗时较长的全局优化手段。
1. 第一阶段,一个平台独立的前段将字节码构造成一种高级中间代码表示(HIR)。
2. 第二阶段,一个平台相关的后端从HIR中产生低级中间代码表示(LIR),而在此之前会在HIR上完成一些优化。
3. 最后节点是在平台相关的后端使用线性扫描算法在LIR上分配寄存器,并在LIR上座窥孔优化,然后产生极其代码。
对Server Compiler则是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器。它会执行所有的经典的优化动作,如:无用代码消除,循环展开,循环表达式外提,公共子表达式消除,常量传播,基本块重排序等,还会实施一些与Java语言特性密切相关的优化技术,如范围检查消除,空值检查消除。
3编译优化技术
JDK设计团队几乎把代码的所有优化措施都集中在了即使编译器,所以一般来说即即时编译器产生的本地代码会比Javac产生的字节码更优秀。接下来介绍几种景点优化技术:
1.公共子表达式消除:如果一个表达式E已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。若这种优化仅限于程序基本块内,称为局部公共子表达式消除;若这种优化的范围涵盖了多个基本块,就称为全局公共子表达式消除。
2.数组边界检查消除:在Java语言中访问数组元素的时候系统将会自动进行上下界的范围检查,即检查i必须满足i>=0 && i 3.方法内联:它除了消除方法调用的成本之外,更重要的意义是为其他优化手段建立良好的基础。由于Java语言中默认的实例方法就是虚方法,对于虚方法,编译器做内联的时候根本就无法确定应该使用哪个方法版本。为了解决虚方法的内联问题,引入了“类型继承关系分析”。编译器在进行内联时,如果是非虚方法,那么直接进行内联,如果是虚方法,则会向CHA查询此方法在当前程序下是否有多个目标版本可供选择,如果只有一个版本,那也可以进行内联,不过这种内联就属于激进优化,需要预留一个逃生门,万一加载了导致继承关系发生变化的新类,那就需要退回到解释状态执行,或者重新编译。 4.逃逸分析:它是为其他优化手段提供依据的分析技术。基本行为就是分析对象动态作用于,当一个对象在方法里面被顶以后,它可能被外部方法所引用,称为线程逃逸。若能证明一个对象不会逃逸到方法或线程之外,就可以进行一些高效的优化,如:栈上分配(对象所占用的内存空间可以随栈帧出栈而销毁,若在堆里分配的话,回收和整理内存都需要消耗时间),同步消除(线程同步本身就是一个相对耗时的过程,若确定不会逃逸出线程,对这个变量就不需要实施同步措施),标量替换(将Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问,若不会逃逸的话,执行程序的时候将可能不创建对象,而直接创建它的若干个被这个方法使用到的成员变量来代替)。 4Java与c/c++编译器对比 Java与c/c++的编译器对比实际上代表了最经典的即时编译器与静态编译器的对比。Java可能会下列原因导致输出本地代码有一些劣势: 1. 首先,因为即时编译器运行占用的是用户程序的运行时间,具有很大的时间压力,它能提供的优化手段也严重受制于编译成本。 2. 其次,Java语言是动态的类型安全语言,这就意味着需要由虚拟机来确保程序不会违反语言的语义或访问非结构化内存。 3. 第三,Java语言中虽然没有virtual关键字,但是使用虚方法的频率却远远大于c/c++语言,这就意味着运行时对方法接受者进行多态选择的频率要远远大于c/c++语言,也以为即时编译器在进行一些优化时的难度远远大于c/c++的静态优化编译器。 4. 第四,Java语言是可以动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,编译器不得不时刻注意并随着类型的变化而在运行时撤销或重新进行一些优化。 5. 第五,Java语言中对象的内存分配都是在堆上进行的,只有方法中的局部变量才能在栈上分撇。