Java语言的“编译期”是一段不确定的过程,因为它可能指的是前端编译器把java文件转变成class字节码文件的过程,也可能指的是虚拟机后端运行期间编译器(JIT)把字节码转变成机器码的过程。
下面讨论的编译期优化指的是javac编译器将java文件转化为字节码的过程,而运行期间优化指的是JIT编译器所做的优化。
编译期优化
虚拟机设计团队把对性能的优化集中到了后端的即时编译器(JIT)中,这样可以让那些不是由javac编译器产生的class文件也同样能享受到编译器优化所带来的好处。但是javac做了许多针对编码过程的优化措施来改善程序员的编码风格和提高编码效率。许多新生的java语法特性,都是靠编译器的“语法糖”来实现,而不是依赖虚拟机的底层改进来支持。
所以说,java中即时编译器在运行期间的优化过程对于程序的运行来说更重要,而前端编译器在编译期的优化过程对于程序编码来说关系更加密切。
javac编译器的编译过程大致可分为三个步骤:
1.解析与填充符号表过程;
2.插入式注解处理器的注解处理过程;
3.语义分析与字节码生成过程。
下面分别来介绍。
解析与填充符号表;
解析步骤包含了词法分析和语法分析两个过程,首先词法分析是将源代码的字符流转变成为标记集合(token),然后语法分析是根据token序列来构造抽象语法树(一种用来描述程序代码语法结构的树状表示方式)。完成词法分析和语法分析之后,下一步是填充符号表,符号表是由一组符号地址和符号信息构成的表格,符号表中所登记的信息在编译的不同阶段都要用到(比如语义分析中符号表所登记的内容将用于语义检查和产生中间代码,目标代码生成阶段当对符号名进行地址分配时,符号表是地址分配的依据)。
插入式注解处理器的注解处理过程:
插入式注解处理器可以看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,那么编译器将回到解析及填充符号表的过程重新处理,直到所有的插入式注解处理器都没有再对语法树进行修改为止。
语义分析与字节码生成过程:
语法分析之后,编译器获得了程序代码的抽象语法树表示,语法树能够表示结构正确的源程序的抽象,但是无法保证源程序是否符合逻辑,而语义分析主要是对结构上正确的源程序进行上下文有关性质的检查。
1.标注检查
标注检查步骤检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配,等等。还有一个重要的动作称为常量折叠也在此阶段完成。
2.数据及控制流分析
数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否有返回值、是否所有的受查异常都被正确处理了等问题。
3.解语法糖
语法糖是指在计算机语言中添加某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。java中的泛型,变长参数,自动拆箱与装箱,条件编译等就属于语法糖,它们在编译阶段就被还原成简单的语法结构(比如List<String>和List<Integer>在运行期间其实是同一个类)。
4.字节码生成
此过程是javac编译过程的最后一个阶段,字节码生成阶段将之前各个步骤所生成的信息转化成字节码写到磁盘中,另外还进行少量的代码添加和转换工作。
运行期优化
在部分商用虚拟机中,java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行特别频繁,就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机就会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器或JIT编译器。
即时编译器并不是虚拟机必须的部分,但是即时编译器编译性能的好坏、代码优化程度的高低确是衡量一款商用虚拟机优秀与否的最关键的指标之一。
众多主流的虚拟机都同时包含解释器和JIT编译器,解释器与JIT编译器各有优势:当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行。当程序运行后,随着事件的推移,JIT编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。
会被即时编译器编译的热点代码有两类:
1.被多次调用的方法体;
2.被多次调用的循环体。
即时编译器会以整个方法作为编译对象,将其编译成机器码。这种编译方式因为编译发生在方法执行过程之中,因此被称作栈上替换(OSR)。
判断一段代码是否是热点代码的方式(热点探测)有两种:
1.基于采样的热点探测:
此方法会周期性检查各个线程的栈顶,如果发现某个或某些方法经常出现在栈顶,那么这个方法就是热点方法。此方法的缺点是很难精确地确认一个方法的热度,容易受到诸如线程阻塞等因素影响。
2.基于计数器的热点探测:
此方法会为每个方法甚至是代码块建立计数器,统计方法的执行次数,如果执行次数超过一个阀值就认为它是热点方法。
注:默认设置下,执行引擎并不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成。当编译工作完成之后,这个方法的调用入口地址就会被系统自动改写成新的地址,下一次调用该方法时就会使用已编译的版本。也就是说,在编译器还未完成之前,执行引擎仍按照解释方式继续执行,而编译动作则在后台的编译线程中进行。
优化技术:
一般来说即时编译器所产生的本地代码会比javac产生的字节码更优秀。即时编译器采用了一系列的技术来优化代码,比如公共子表达式消除,数组范围内检查消除,方法内联,逃逸分析等。