Java虚拟机之编译期及其优化
1、有三种可能的编译期
-
I.前端编译器将.java文件转换成.class文件的过程
-
II.虚拟机的后端运行期编译器(JIT编译器:Just In Time Complier)把字节码转换成机器码的过程
-
III.静态提前编译器(AOT编译器:Ahead Of Time Complier)直接把.java文件转换成本地机器代码的过程
而一般认知中的编译期指的就是第一类编译期(下文提到的编译期皆为此编译期),而第二类编译期有另一个更加浅白的说法:运行期。
2、三种编译期具有代表性的编译器如下:
-
I.前端编译器:Javac,ECJ(eclipse的增量式编译器)
-
II.后端运行期编译器:HotSpot的C1,C2编译器
-
III.AOT编译器GNU Complier for the java(GCJ),Excelsior JET
3、编译期执行过程
该过程中值得注意的点如下:
-
I.标记是程序编译过程中最小的元素,“int a=b+2”中包含6个标记。
-
II.插入式注解处理器可以任意读取、修改和添加抽象树中的元素,因此一旦插入式注解处理器对语法树做出了修改,那么编译期将回到解析及填充符号表的过程直到所有插入式注解处理器没有对语法树做出修改。编译期几乎没有任何优化措施,而插入式注解处理器这个特性让我们有了在编译期对编译期进行干涉的机会。
-
III.在标记检查阶段会出现常量折叠,如int a=1+2;会直接替换为int a=3;
4、几颗语法糖
语法糖指的是语言层面存在,但字节码层面不存在的特殊语法,是由某些常用的若干条语句封装成的一条语句。
Java中的几颗语法糖
-
I.伪泛型和类型擦除 VS 真实泛型于类型膨胀
-
C#中存在的泛型是真实泛型,在源码和IL及CLR中真实存在,在运行期同一种泛型填入不同的类型后相互是不同的类,即List和List在运行期是不同的类型,填入类型后产生了不同类的过程称为类型膨胀。
-
Java中存在的泛型是语法糖,只在源码中存在,在运行时泛型填入的类型会被擦除,而在需要用到该填入类型后的泛型的地方,该泛型被替换为强转后的原生类型,因此在运行期同一种泛型填入不同的类型后相互是相同的类,即List和List在运行期是相同的类型。这种泛型称为伪泛型,填入类型后填入的类型会被擦除的过程称为类型擦除。
Java泛型的特性会导致在泛型重载时出错,但我们可以利用class文件结构中方法的唯一性由特征签名和返回值保证的特点,在遇到反省重载时添加不同的返回值来保证重载成功。
-
-
II.自动拆箱和自动装箱
自动装箱、拆箱典型例子就是包装类的valueOf(),xxxValue()方法——在源码中存在,但在运行时会被替换成强制转换后的基本类型。值得注意的是,包装类的“==”符号遇到算数运算符才自动拆箱,而equals()方法不会处理数据转型问题。
-
III.迭代器也是一个语法糖,它在运行时会被替换成循环结构,与一般的循环语句无异。
-
IV.变长参数,即方法参数列表中出现的允许传入多个同类型数据的参数,如Object… object这样的参数,在运行时会被封装成数组类型的参数。
-
V.并不常见的条件编译:在Java中有且仅有一种条件编译语法——if(true){}else{},它在运行期生成的字节码只包括一种情况下的语句,如
if(true){System.out.println(“true”);}else{System.out.println(“else”);}在编译后反编译结果为:System.out.println(“true”);