一、概述
1、三类编译过程
#前端编译器:把*.java文件编程*.class文件的过程。——典型代表:Sun的javac、Eclipse JDT中的增量式编译器(ECJ)
#后端运行期编译器(JIT编译器): 把字节码转变为机器码的过程。——典型代表:HotSpot VM的C1、C2编译器
#静态提前编译器(AOT编译器):直接把*.java文件编译成本地机器代码的过程。——典型代表:GNU Compiler for the java(GCJ)
2、本章讨论的是第一类。javac这类编译器对代码的运行效率几乎没有任何优化措施。虚拟机设计团队把对性能的优化集中到了后端的即时编译器中,这样可以让那些不是由javac产生的Class文件也同样能够享受到编译器优化带来的好处。
3、Java中即时编译器在运行期的优化过程对于程序运行来说更重要,而前端编译器在编译器的优化过程对于程序编码来说关系更密切。
4、javac做了很多针对java语言编码过程的优化措施来改善程序员的编码风格和提高编码效率。相当多新生的Java语法特性都是靠编译器的“语法糖”来实现,而不是依赖虚拟机的底层改进来支持。
二、Javac编译器
1、Javac的源码
1.1.编译过程
#解析与填充符号表过程
#插入式注解处理器的注解处理过程
#分析与字节码生成过程
2、解析与填充符号表
2.1.词法、语法分析
#词法分析作用:是将源代码中的字符流转变为标记(Token)集合。
#单位:单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符等都可以成为标记。
#语法分析作用:根据Token序列构造抽象树
#抽象语法树:一种用来描述程序代码结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如:包、类型、修饰符、运算符、接口、返回值甚至代码注释等。
#后续的操作都建立在抽象语法树上
2.2.填充符号表
#符号表结构:是由一组符号地址和符号信息构成的表格,读者可以把它想象成哈希表中K-V值对的形式。
#符号表作用:符号表中所登记的信息在编译的不同阶段都要用到。A、语义分析中,符号表所登记的内容将用于语义检查和产生中间代码;B、目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。
3、注解处理器
3.1.作用时间:与普通的Java代码一样,是在运行期发挥作用的。
3.2.作用:可以读取、修改、添加抽象语法树中的任意元素。一旦这些插件在注解期间对语法书进行了修改,编译器将回到解析和填充符号表的过程进行重新处理,知道所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个Round。
4、语义分析与字节码生成
语义分析的主要任务:对结构上正确的源程序进行上下文有关性质的审查,如类型审查。
4.1.标注检查
内容:包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配。
常量重叠:
例如,我们在Java代码中写了如下定义:int a=1+2;那么在语法树上仍然能看到字面量“1”、“2”已经操作符“+”。但是经过常量重叠之后,它们将会被折叠为字面量“3”。
4.2.数据及控制流分析
作用:对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理等问题。
区别:编译时期的数据及控制流分析与类加载时的数据及控制流分析的目的基本上是一致的,但校验范围有所区别,有一些校验项只有在编译器或运行期才能进行。比如,将局部变量声明为final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障。(在Class文件中不可能知道一个局部变量是不是声明为final,因为没有相关的标志位)
4.3.解语法糖
语法糖:指在计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。——作用:增加程序的可读性,从而减少程序代码出错的机会。
Java中常用的语法糖:Java在现代编程语言中属于“低糖语言”。常用的主要是泛型、变长参数、自动装箱/拆箱等。
解语法糖:虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。
4.4.字节码生成
内容:字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化为字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。
添加的内容:实例构造器<init>()方法和类构造器<clinit>()方法就是这个阶段添加到语法树中的。(注意,这里的实例构造器并不是指默认构造函数,如果用户代码中没有提供任何构造函数,那编译器将会添加一个没有参数的、访问性与当前类一致的默认构造函数,这个工作在填充符号表阶段就已经完成)。
代码收敛的过程:把语句块({},static{})、变量初始化、调用父类的实例构造器等操作收敛到<init>()和<clinit>()方法之中,并且保证一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序进行。
程序的逻辑优化:如把字符串的加操作替换为StringBuffer或StringBuilder的append()操作等。
最后由ClassWrite类的writeClass方法输出字节码,生成最终的Class文件,到此为止,整个编译过程宣告结束。
三、Java语法糖的味道
语法糖可以看做是编译器实现的一些“小把戏”,这些“小把戏”可能会使得效率“大提升”,但我们也应该去了解这些“小把戏”背后的真实世界,那样才能利用好它们,而不是被它们所迷惑。
1、泛型与类型擦除
1.1.本质:参数化类型的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以应用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
1.2.原本的方法:在没有泛型以前,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。——问题:在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会嫁接到程序运行期之中去。
1.3.泛型分类
#真实泛型:在C#语言里面,泛型无论在程序源码中、编译后的IL中(中间语言,这时候泛型是一个占位符),或是运行期的CLR中,都是切实存在的,List<int>与List<String>就是两种不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
#伪泛型:在Java语言里面,泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型了,并且在相应的地方插入了强制转型代码。因此对于运行期的Java语言来说,ArrayList<int>与ArrayList<String>就是同一个类。所以泛型技术实际上是Java语言的一个语法糖。Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
#存在的问题:
当泛型遇见重载
如上的代码是无法通过编译的,因为在编译之后参数ArrayList<String>和ArrayList<Integer>都被擦除了,变成了一样的原生类型ArrayList<E>,擦除动作导致这两种方法的特征签名变得一模一样。因此无法重载。
接着我们给这两个方法添加不同的返回值,发现在JDK1.8中,同样是无法通过编译的,跟书上所说有所不同。应该是对这部分进行了一些修改。
总结:擦除法所谓的擦除。仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能够通过反射手段取得参数化类型的根本依据。
2、自动装箱、拆箱与遍历循环
自动装箱、拆箱在编译之后被转化为了对应的包装和还原方法,如Integer.valueOf()与Integer.intValue()方法。
遍历循环则把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。
变长参数在调用的时候变成了一个数组类型的参数。
3、条件编译
Java语言并没有使用预处理器,因为Java语言天然的编译方式(编译器并非一个个地编译Class文件,而是将所有编译单元的语法树顶级节点输入到待处理列表后再进行编译,因此各个文件之间能够互相提供符号信息)无需使用预处理器。C、C++的预处理器最初的任务是解决编译时的代码依赖关系。(如#include预处理命令)
Java语言当然也可以进行条件编译,方法就是使用条件为常量的if语句。如下图,但是此代码中的if语句不同于其他Java代码,它在编译阶段就会被“运行”,生成的字节码中只包括“System.out.println("block1");”一条语句,而不会涉及if语句及另外一个分子中的代码块。
package improve;
public class Test2 {
public static void main(String[] args){
if(true){
System.out.println("block1");
}else{
System.out.println("block2");
}
}
}
如下,如果换成while,可能被拒绝编译,如下
Java语言中条件编译的实现,也是Java语言的一颗语法糖,根据布尔值的真假,编译器将会把分支中不成立的代码消除掉,这一工作将在编译器解除语法糖阶段完成。局限性:由于这种条件编译的实现方式使用了if语句,所以它必须遵循最基本的Java语法,只能写在方法体内部,因此它只能实现语句基本块级别的条件编译,而没有办法实现根据条件调整整个结构。
其他的一些语法糖:内部类、枚举类、断言语句、对枚举和字符串的switch支持、try语句中定义和关闭资源等。
博客内容来自《深入理解Java虚拟机》