三个常见的编译方式:
? 我们可以这样认为, Java 中即时编译器在运行期的优化过程,支撑了程序执行效率的不断提升;而前端编译器在编译期的优化过程,则是支撑着程序员的编码效率和语言使用者的幸福感的提高。
从 Javac 代码的总体结构来看,编译过程大致可以分为 1 个准备过程和 3 个处理过程,它们分别如下所示:
1.词法、语法分析
? 词法分析是将源代码的字符流转变为标记(Token)集合的过程,单个字符是程序编写时的最小元素,但标记才是编译时的最小元素。关键字、变量名、字面量、运算符都可以作为标记,如“int a=b+2”这句代码中就包含了 6 个标记,分别是 int、 a、 =、 b、+、 2,虽然关键字 int 由 3 个字符构成,但是它只是一个独立的标记,不可以再拆分。在 Javac 的源码中,词法分析过程由 com.sun.tools.javac.parser.Scanner 类来实现。
? 语法分析是根据标记序列构造抽象语法树的过程,抽象语法树( Abstract SyntaxTree, AST)是一种用来描述程序代码语法结构的树形表示方式,抽象语法树的每一个
节点都代表着程序代码中的一个语法结构(Syntax Construct),例如包*、类型、修饰*
符、运算符、接口、返回值甚至连代码注释等都可以是一种特定的语法结构。这个阶段产出的抽象语法树是以 com.sun.tools.javac.tree.JCTree 类表示的。
2.填充符号表
? 符号表( Symbol Table)是由一组符号地址和符号信息构成的数据结构,可以把它类比想象成哈希表中键值对的存储形式。填充符号表的过程由 com.sun.tools.javac.comp.Enter 类实现。**符号表中所登记的信息在编译的不同阶段都要被用到。**譬如在语义分析的过程中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的声明是否一致)和产生中间代码,在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的直接依据。
可以把插入式注解处理器看作是一组编译器的插件当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环过程称为一个轮次(Round)。
Javac 在编译过程中,语义分析过程可分为标注检查和数据及控制流分析两个步骤。
1.标注检查
? 标注检查步骤要检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配。还会顺便进行一个称为常量折叠(Constant Folding)的代码优化,这是Javac 编译器会对源代码做的极少量优化措施之一。
int a = 1 + 2;
则在抽象语法树上仍然能看到字面量“1”“2”和操作符“+”号,但是在经过常量折叠优化之后,它们将会被折叠为字面量“3”.
2.数据及控制流分析
? 数据流分析和控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。编译时期的数据及控制流分析与类加载时的数据及控制流分析的目的基本上可以看作是一致的.
**注意:**有关final 的验证
? 可以肯定地推断出把局部变量声明为 final,对运行期是完全没有影响的,变量的不变性仅仅由 Javac 编译器在编译期间来保障,这就是一个只能在编译期而不能在运行期中检查的例子
3.解语法糖
通常来说使用语法糖能够减少代码量、增加程序的可读性,从而减少程序代码出错的机会。
在 Javac 的源码中,解语法糖的过程由 desugar()方法触发,在com.sun.tools.javac.comp.TransTypes 类和 com.sun.tools.javac.comp.Lower 类中完成。
4.字节码生成
? 字节码生成是 Javac 编译过程的最后一个阶段,在 Javac 源码里面由com.sun.tools.javac.jvm.Gen 类来完成。字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码指令写到磁盘中,编译器还进行了少量的代码添加和转换工作。
? 实例构造器<init>()方法和类构造器<clinit>()方法就是在这个阶段被添加到语法树之中的。
<init>()和<clinit>()这两个构造器的产生实际上是一种代码收敛的过程,编译器会把语句块(对于实例构造器而言是“{}”块,对于类构造器而言是“static{}”块)、变量初始化(实例变量和类变量)、 调用父类的实例构造器(仅仅是实例构造器, <clinit>()方法中无须调用父类的<clinit>()方法, Java 虚拟机会自动保证父类构造器的正确执行,但在<clinit>()方法中经常会生成调用 java.lang.Object 的<init>()方法的代码)等操作收敛到<init>()和<clinit>()方法之中并且保证无论源码中出现的顺序如何,都一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序进行。