一、编译器
Java语言的编译器有:
前端编译器:把*.java文件转变成*.class文件的过程,例如:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)。
后端编译器(也称为JIT编译器,Just In Time Compiler):把字节码转变成机器码的过程,例如:HotSpot VM的C1、C2编译器
静态提前编译器(AOT编译器,Ahead Of Time Compiler):把*.java文件编译成本地机器代码的过程,例如:GCJ、Excelsior JET。
注:虚拟机设计团队对性能的优化集中在后端的即时编译器中,像Javac这样的前端编译器主要针对编码过程的优化,来改善程序员的编码风格和提高编码效率。
二、Javac编译器
Javac编译器的编译过程大致分为三个过程:
** 解析与填充符号表
** 插入式注解处理器的注解处理
** 分析与字节码生成
Javac的编译过程示意如下:
1、解析与填充符号表
解析主要是进行词法分析和语法分析;
解析之后会填充符号表,符号表中所登记的信息在编译的不同阶段都要用到。
2、注解处理器
通过一组插入式注解处理器的标准API,可以在编译器对注解进行处理。可以把注解处理器看作是编译器的插件。
3、语义分析与字节码生成
语法分析的结果是抽象语法树,语义分析以抽象语法树为基础,进行语义分析。主要分析点有:标注检查、数据及控制流分析
语义分析之后会进行“解语法糖”步骤;
之后是字节码生成
三、Java语法糖
语法糖(Syntactic Sugar),也称糖衣语法。指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,主要目的是方便程序员使用、增加程序可读性、减少程序代码出错的机会。
Java中最常用的语法糖有:Java伪泛型、变长参数、自动装箱拆箱、遍历循环等。
1、Java伪泛型
目前计算机语言实现泛型的技术有两种:一种是称为“类型膨胀”,另一种称为“类型擦除”。
C++、C#中的泛型无论在程序源码中、编译后的文件中、运行时内存中都是真实存在的。例如List<int>和List<String>在运行时就是两种不同的类型。这种实现称为“类型膨胀”,基于这种方法实现的泛型称为“真实泛型”。
Java中的泛型只存在于源文件中,在编译后的字节码文件中,就已经被替换为原来的原生类型,并插入强制转型代码。这种实现方法称为"类型擦除",基于这种方法的泛型称为“伪泛型”。
真实泛型不是语法糖,Java里面的伪泛型时一种语法糖。
注:类型查查所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息(Signature、LocalVariableTypeTable等属性),这也是我们能通过反射手段得到参数化类型的根本依据。
2、自动装箱、拆箱和循环遍历
如下是编译前的代码(解语法糖之前):
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
int sum = 0;
for (int i : list) {
sum += i;
}
System.out.println(sum);
}
下面是编译后反编译的代码(解语法糖之后):
public static void main(String[] args) {
List list = Arrays.asList( new Integer[] {
Integer.valueOf(1),
Integer.valueOf(2),
Integer.valueOf(3),
Integer.valueOf(4)
});
int sum = 0;
for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
int i = ((Integer)localIterator.next()).intValue();
sum += i;
}
System.out.println(sum);
}
从上面代码可以看出:解语法糖之后,遍历循环是把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。
变长参数是在解语法糖之后变成了一个数组类型的参数。
3、条件编译
C、C++中使用预处理器指示器来完成条件编译。C、C++的预处理器最初的任务是解决编译时的代码依赖关系。
而在Java中并没有预处理器,因为Java并非一个一个地编译Java文件,而是将所有的编译单元的语法树顶级节点输入到待处理列表后再进行编译,因此各个文件之间能够相互提供符号信息,因此也就不需要使用预处理器。
在Java中使用条件编译,方法是使用条件为常量的if语句,解语法糖时,会根据布尔常量值的真假,编译器会把分支不成立的代码块消除掉。
Java的语法糖,还有内部类、枚举类、断言语句等等。
四、即时编译器
1、概述
即时编译器(JIT编译器)并不是虚拟机必须的部分。但是,即时编译器性能的好坏、代码优化程序的高低却是衡量一款商用虚拟机优秀 与否的最关键指标。
在部分商用虚拟机中,Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某些代码的运行特别频繁时,为了提高这些代码的执行效率,会在运行时,将这些代码编译成本地机器码,并进行各种层次的优化,这个任务就是有即时编译器完成的。
HotSpot虚拟机中内置了两个即时编译器,称为Client Compiler和Server Compiler,或者简称C1编译器和C2编译器。默认情况下是采用解释器和其中一个编译器直接配合工作。用户可以使用-client或-server参数去强制指定虚拟机运行在Client模式还是Server模式。
也可以改变虚拟机的运行模式:默认模式是“混合模式”(Mixed Mode)(即解释器与编译器混合使用);使用参数-Xint可以强制虚拟机运行于“解释模式”(Interpreted Mode),也可以使用参数-Xcomp强制虚拟机运行于“编译模式”(Compiled Mode)。
2、编译对象与触发条件
<1>、编译对象
所谓的“热点代码”有两类:被多次调用的方法、被多次执行的循环体
针对热点代码,JIT编译器会将热点代码所在的方法作为编译对象。
<2>、触发条件
触发条件是通过热点探测(Hot Spot Detection)来实现的。
目前的热点探测判定方式有两种:基于采样的热点探测(Sample Based Hot Spot Detection)和基于计数器的热点探测(Counter Based Hot Spot Detection).
前者的优点是简单高效,还可以获取方法的调用关系,缺点是很难精确地确认一个方法的热度。后者的优点是可以相对精确地获得代码的热度,缺点是不能直接获取方法的调用关系,实现复杂。
HotSpot虚拟机使用了基于计数器的热点探测方法。每个方法都有两个计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。
前者很好理解,后者用于统计一个方法中循环体代码执行的次数,在字节码中于丹控制流向后跳转的指令就称为“回边(Back Edge)”。
3、编译过程
Server Compiler和Client Compiler两个编译器的编译过程是不一样的。对于Client Compiler来说,它是一个简单快速的三段式编译器,主要关注点是局部性的优化。
对于Clieng Compiler:
第一阶段:一个平台独立的前端将字节码构造成一种高级中间代码表示(High-Level Intermediate Representaion, HIR);
第二阶段:一个平台相关的后端从HIR中产生低级中间代码表示(Low-Level Intermediate Representation, LIR);
第三阶段:在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窥孔优化,然后产生机器代码。
示意图如下:
对于Server Compiler
Server Compiler是专门面向服务端的典型应用并为服务器的性能配置特别调整过的编译器。
Server Compiler的速度比较缓慢,但它的速度仍然远远超过传统的静态优化编译器,相对于Client Compiler编译输出的代码质量有所提高,可以减少本地代码的执行时间,从而抵消了额外的编译时间开销。
4、查看与分析即时编译结果
查看被JIT编译器编译的方法:
使用参数:-XX:+PrintCompilation ,可以使虚拟机在即时编译时将被编译成本地代码的方法名称打印出来。
使用参数:-XX:+PrintInlining,可以使虚拟机输出方法内联信息。
查看JIT编译器编译的方法生成的本地代码:
首先,要为虚拟机安装反汇编适配器(放在JRE/bin/client 或 /servier目录下);
然后使用-XX:+PrintAssembly,使虚拟机打印编译方法的汇编代码,也可以使用-XX:printOptoAssemby(用于Server VM)或XX:+print LIR(用于Client VM)来输出比较接近最终结果的中间代码表示。注:需要FastDebug版的虚拟机, 如果是Product版虚拟机,需要加入参数-XX:+UnlockDiagnosticVMOptions打开虚拟机诊断模式后才能使用。
查看本地代码的生成过程:
-XX:+PrintCFGToFile(使用Client Compiler)或-XX:PrintIdealGraphFile(使用Server Compiler),使虚拟机将编译过程中各个阶段的数据输出到文件。
这些文件可以使用Java HotSpot Client Compiler Visualizer(使用Client Compiler)或Ideal Graph Visualizer(使用Server Compiler)打开。
注:要输出CFG或IdealGraph文件。需要Debug版虚拟机支持,Product版或FastDebug版虚拟机无法输出这些文件。
5、即时编译优化技术
五、总结
Javac编译器和JIT编译器的执行过程合并起来其实就等同于一个传统的编译器所执行的编译过程。