六 早期(编译期)优化
1 “编译期”的含义
· 可能是指一个前端编译器把*.java文件转变成*.class文件的过程,前端编译器如:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ);
· 也可能是指虚拟机的后端运行期编译器(JIT编译器)把字节码转变成机器码的过程,JIT编译器如:HotSpot VM的C1、C2编译器;
· 还可能是指使用静态提前编译器(AOT编译器)直接把*.java文件编译成本地机器码的过程,AOT编译器如:GNU Compiler for the Java(GCJ)、Excelsior JET。
本部分的编译期指第一种。
前端编译器在编译期的优化与程序编码关系更加密切,JIT编译器在运行期的优化对程序运行来说更加重要。
2 Javac编译器
---Javac编译器是一个使用Java语言编写的程序。
---编译过程大致可以分为:
· 解析与填充符号表过程;
· 插入式注解处理器的注解处理过程;
· 分析与字节码生成过程。
---这三个步骤之间的关系与交互顺序如下图:
(1)解析与填充符号表
1)词法分析和语法分析
---词法分析:将源代码的字符流转变成标记(Token)集合。
---单个字符是程序编写过程的最小元素,标记是编译过程的最小元素。
---标记包括:关键字、变量名、字面量、运算符。
---语法分析:根据标记序列构造抽象语法树的过程。
---抽象语法树:一种用来描述程序代码语法结构的树形表示方式。语法树的每一个节点都代表着程序代码中的一个语法结构,如包、类型、修饰符、运算符、接口、返回值、代码注释等。
2)填充符号表
---符号表:由一组符号地址和符号信息构成的表格。
---符号表中所登记的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检查和产生中间代码;在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。
(2)注解处理器
---在编译期间对注解进行处理,可以读取、修改、添加抽象语法树中的任意元素。若在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。
(3)语义分析和字节码生成
---语义分析:对结构上正确的源程序进行上下文性质的审查,如进行类型审查。
---语义分析包括:标注检查和数据及控制流分析两步。
1)标注检查
---检查的内容包括如变量使用前是否已经被声明、变量与赋值之间的数据类型是否能够匹配、常量折叠等
2)数据及控制流分析
---是对程序上下文逻辑更进一步的验证,可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都别正确处理了等问题。
3)解语法糖
---Java中常用的语法糖:泛型、变长参数、自动装箱/拆箱、遍历循环、条件编译、内部类、枚举类、断言语句、对枚举和字符串的switch支持、try语句中定义和关闭资源等。
---解语法糖:虚拟机运行时不支持语法糖的语法,它们在编译阶段被还原回简单的基础语法结构。
4)字节码生成
---不仅仅把前面各个步骤所生成的信息转换成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。
---实例构造器<init>()方法和类构造器<clinit>()方法就是在这个阶段添加到语法树之中的。这里的实例构造器不包括默认构造函数,默认构造函数是在填充符号表阶段添加完成的。
---<init>()方法和<clinit>()方法实际上是一个代码收敛的过程,编译器会把语句块({}块或static{}块)、变量初始化(实例变量和类变量)、调用父类的实例构造器(<clinit>()方法中无须调用父类的<clinit>()方法)等操作收敛到<init>()方法和<clinit>()方法中。
---代码替换例子:把字符串的加操作替换成StringBuffer或StringBuilder(取决于JDK版本是否大于等于JDK1.5)的append()操作。
3 Java语法糖
(1)泛型与类型擦除
---真实泛型:泛型无论在程序源码中、编译后的中间代码中,还是运行期的代码中,都是切实存在的,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
---伪泛型:只在程序源码中存在,在编译后的字节码文件中就已经替换为原生类型了,并且在相应的地方插入了强制转型代码,这种实现称为类型擦除,基于这种方法实现的泛型称为伪泛型。
---Signature属性存储了一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。
---擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息(在Signature属性中),这也是我们能通过反射手段取得参数化类型的根本依据。
(2)自动装箱、拆箱、遍历循环与变长参数
---遍历循环需要被遍历的类实现Iterable接口的原因:遍历循环在编译之后会还原成迭代器的实现。
---变长参数在编译后会还原成数组实现。
---自动拆、装箱的陷阱:
· 当两个包装类进行比较时,包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱;
· 当两个包装类进行比较或一个包装类与另一个基本数据进行比较时,它们的equals()方法不会处理数据转型的关系;
· 当一个包装类与一个基本数据类型进行比较或两个基本数据类型进行比较时,"=="运算会自动拆箱、自动类型转换;
· 对于Integer类,当值在-128-127之间时,会使用Integer.valueOf()方法直接从缓存中取出相应对象;而当值不在这个范围内时,会使用Integer.valueOf()方法new一个Integer对象。
---程序代码为:
---执行结果为:
(3)条件编译
---只能使用条件为常量的if语句才能实现,如果使用常量与其他带有条件判断能力的语句搭配,则可能在控制流分析中提示错误,被拒绝编译。