java 编译期有三种含义:
编译期 | 含义 |
---|---|
前期编译器 | 将 .java 转换为.class 文件的过程 |
后端运行期编译器 | 将字节码转换为机器码的过程 |
静态前期编译器 | 将 .java 编译为机器码的过程 |
各编译器的代表性编译器:
时期 | 编译器名称 |
---|---|
前期 | Sun Javac、Eclipse JDT 的增量式编译器 |
后端运行期 | HotSpot 的 C1、C2 编译器 |
静态提前编译 | GCJ、Excelsior JET |
虚拟机设计团队将性能优化集中于后端即时编译器中,由此非 javac 产生的 Class 文件也可以被优化。但是 Javac 有许多针对语言编码过程的优化措施来改善编码风格并提高效率。很多新生语法特性都是靠编译器的语法糖来实现。
前端编译器的优化与程序编码更为密切,后期运行期优化与程序运行更为密切。
Javac 编译器
分为三个编译阶段:
阶段 | 具体内容 |
---|---|
1 | 解析与填充符号表 |
2 | 插入式注解处理器的注解处理 |
3 | 分析并生成字节码 |
解析与填充符号表
1.解析
包括词法分析和语法分析两个部分
词法分析将源代码字符流转变为标记集合,单个字符是程序编写过程的最小元素,标记是编译过程的最小元素。关键字、变量名、字面量、运算符都可成为标记。
语法分析根据标记序列构造抽象语法树(用于描述程序代码语法结构的树形表示方式),其中的每一个节点都代表一个语法结构,如包、类型、运算符、修饰符、接口、返回值、甚至于代码注释。
阶段 | 实现类 |
---|---|
词法分析 | com.sun.tools.javac.parser.Scanner |
语法分析 | com.sun.tools.javac.parser.Parser(产生出的语法树由 com.sun.tools.javac.tree.JCTree 类表示) |
语法分析后,编译器不再对源码进行操作,而是对抽象语法树进行操作。
2.填充符号表
符号表由符号地址及符号信息构成,实现形式多样化,树状、栈结构、哈希表、有序表等
内容在编译的各个时期均需要使用(由 com.sun.tools.javac.comp.Enter 类进行内容填充)
阶段 | 符号表作用 |
---|---|
语义分析 | 用于语义检查以及产生中间代码 |
目标代码生成 | 依据符号表对符号名进行地址分配 |
符号表填充的出口是一个待处理列表,包含了每一个编译单元的抽象语法树的顶级节点以及 package-info.java 的顶级节点。
注解处理器
注解在运行期间发挥作用,JDK 1.6 提供了一组插入式注解处理器的标准 API 在编译期间对注解进行处理。通过这些 API 可以读取、添加并修改语法树中的任意元素,而如果对语法树进行了修改,编译器将重新开始处理流程,直到没有再发生修改操作为止。
Javac 中注解处理标准 API 执行涉及方法:
阶段 | 使用方法 |
---|---|
初始化 | initProcessAnnotations() |
执行 | ProcessAnnotations() |
processAnnotations() 方法判断是否还有新的注解处理器需要执行,如有,则通过 com.sun.tools.javac.processing.JavacProcessingEnvironment 的 doProcessing() 生成新的 JavaCompiler 对象对后续步骤进行处理。
语义分析
1.标注检查(实现类为 com.sun.tools.javac.comp.Attr 以及 com.sun.tools.javac.comp.Check)
检查内容包括:变量使用前是否声明,变量与赋值之间的数据类型是否匹配等。其中有一个重要的步骤:常量折叠。
比如:int a = 1 + 2; 这条语句在语法树上能看到 “1”,“2”,“+”,而经过常量折叠后,将会变成字面量“3”。由此在代码中这两种定义的运算量相同。
2.数据及控制流分析(实现类为 com.sun.tools.javac.comp.Flow)
检查内容包括:局部变量使用前是否赋值,方法每条路径是否都有返回值,是否所有的受检查异常都被正确处理。该步骤与类加载时期的数据及控制流分析目的基本一致,只是校验范围有区别。某些校验项只有在运行期才能进行。
3.解语法糖(实现类为 com.sun.tools.javac.comp.TransTypes 和 com.sun.tools.javac.comp.Lower)
jvm 运行时不支持语法糖,在编译阶段会被还原为简单的基础语法结构。
4.字节码生成(实现类为 com.sun.tools.javac.jvm.Gen)
该阶段将前面生成的所有信息转换为字节码写入磁盘,并进行少量代码添加与转换的工作。实例构造器与类构造器均于该阶段加入语法树中(此处实例构造器并非默认构造函数,如果代码中没有提供构造函数,则编译器将添加一个无参数且访问性与当前类一致的默认构造函数,该工作于符号填充阶段已经完成)。
这两个构造器的产生过程实际上是代码收敛的过程,编译器将语句块(实例构造器的“{}”块,类构造器的“static{}”块)、变量初始化(实例变量与类变量)、调用父类实例构造器(仅为实例构造器《client》()方法无需调用父类的《client》()方法,虚拟机自动保证父类构造器执行,但在《clinit》()方法中经常会生成调用 java.lang.Object 的 《init》() 的代码)
泛型与类型擦除
在 jdk1.5 之前,由于所有类型均继承于 Object, 所以 Object 可能转型为任意对象,因此,只有开发人员及运行期的虚拟机知道具体是什么类型。编译期间编译器是无法检测对象是否转型成功的,也因此只能依赖开发人员保证操作的正确性,许多类型转换的风险就被转嫁于运行期。
C# 的泛型在源码,编译后的中间语言以及运行期 CLR 中均真实存在,List<“int”> 与 List<“String”> 皆真实存在,都在运行期生成,有自己的虚方法表及数据类型。此种方式为“类型膨胀”,称为真实泛型。
java 的泛型只在源码中存在,编译后就已经替换为原生类型,并在相应位置插入了强制转型代码。在运行期,List<“int”> 与 List<“String”> 是同一个类,此种泛型实质是语法糖,方式为“类型擦除”,称为伪泛型。
java 的伪泛型会导致无法重载,因为被擦除后,类型变得完全一样。但是通过使用不同的返回值,就可以共存于同一个 class 文件中(然而只有 javac 编译器才支持)。此后 JCP 组织加入了 Signature 属性用于存储字节码层面的特征签名,保存了参数化类型的信息,内容包括:方法名称、参数顺序、参数类型、方法返回值及受查异常表(该属性的出现表明 java 泛型实际在元数据中保留了泛型信息,这也是能通过反射获得参数化类型的根本依据)。
自动装箱与循环遍历
自动装箱拆箱经过编译后都变为对应的包装和还原方法,循环遍历的代码在经过编译后会被还原为迭代器的实现,这也是循环遍历要实现 Iterable 接口的原因。
条件编译
java 并不像 C 那样使用预处理器指示符完成条件编译,因为 java 编译的方式是将所有编译单元的语法树顶级节点输入处理列表然后再编译,因此各文件间能够互相提供符号信息。
java 也可以通过使用条件为常量的 if 语句实现条件编译,if(true){.....}else{.....}
,此时只有 if 当中的语句会在编译期间被执行,而 else 中的语句则不会被执行。而对于其它具有判断能力的语句,如 while,使用常量作为判断表达时,会直接被拒绝编译。
对于条件编译中不成立的分支部分,编译器将在解语法糖阶段将其消除。这种条件编译由于使用了 if 语句,因此只能实现语句基本块级别的条件编译,无法调整类的结构。