Java 的编译过程
即使编译器在运行期的优化支持了程序执行效率的提升,而前端编译器在编译期的优化支持了程序员的编码效率和语言使用幸福感的提高;
JDK 6 以前
,javac 不属于标准 Java SE API,代码独立存放在 tools.jar
,使用时需要将路径加入 ClassPath;
JDK 6 开始
,javac 晋升成标准 Java 类库,源码放在 JDK_SRC_HOME/langtools/src/share/classes/com/sun/tools/javac
;
JDK 9 开始
,整个 JDK 的 Java 类库模块化重构,javac 编译器放在 jdk.compiler
模块,存放路径为 JDK_SRC_HOME/src/jdk.compiler/share/classes/com/sun/tools/javac
;
OpenJDK 源码
可以直接执行 javac.Main 的 main() 方法来执行编译,参数与直接使用 javac 命令一致;
从 javac 代码的总体结构看,编译过程大致可以分为 1 个准备过程和 3 个处理过程;
准备过程
: 初始化插入式注解处理器;解析与填充符号表
过程,包括:插入式注解处理器的注解处理
过程: 插入式注解处理器的执行阶段,影响 javac 的编译行为;分析与字节码生成
过程,包括:插入式注解可能会产生新的符号,如果有新的符号产生,就必须转回解析、填充符号表的过程重新处理新的符号;
javac 编译入口代码在 com.sun.tools.javac.main.JavaCompiler
类的 compile() 方法;
parseFiles
: 1.1,词法分析、语法分析;enterTrees
: 1.2,输入到符号表;processAnnotations
: 2,执行注解处理;a. 词法、语法分析
词法分析
,将源代码的字符流转变成标记(Token)集合,字符是程序编写的最小单元,而 Token 是编译的最小单元;关键字、变量名、字面量、运算符等都是 Token,不可再拆分;javac 的词法分析由 com.sun.tools.javac.parser.Scanner
类实现;语法分析
,根据 Token 序列构造抽象语法树(Abstract Syntax Tree,AST,描述一个结构正确的源程序,程序语言结构的树形表示),树的每一个节点代表着程序的一个语法结构(Syntax Construct);包、类型、修饰符、运算符、接口、返回值、代码注释等,都可以是一种特定的语法结构;javac 的语法分析由 com.sun.tools.javac.parser.Parser
类实现,抽象语法树以 com.sun.tools.javac.tree.JCTree
类表示;b. 填充符号表
com.sun.tools.javac.comp.Enter
类实现;Java 在 JDK 5 开始支持注解(Annotations),原只对程序运行期间发挥作用;到 JDK 6 时添加了插入式注解处理器
的标准 API,提前至编译期处理特定注解,可以影响前端编译器的工作过程;
插入式注解处理器相当于编译器的插件,通过这些插件可以读取、修改、添加抽象语法树的任意元素;若插件在处理注解期间对抽象语法树进行修改,编译器将回退至解析和填充符号表的过程,直到插入式注解处理器不再修改抽象语法树;每一次循环称为一个轮次(Round);
编码效率工具 Lombok 通过注解实现自动生成 getter/setter 方法、空置检查、生成受查异常表、生成 equals() 和 hashCode() 等功能,都是依赖插入式注解处理器实现的;
initProcessAnnotations
: 准备过程,初始化插入式注解处理器;processAnnotations
: 完成插入式注解执行处理;若有新的注解处理器需要执行,则通过 com.sun.tools.javac.processing.JavacProcessingEnvironment
类的 doProcessing()
生成一个新的 JavaCompiler
对象,进行后续的编译已处理;语义分析
,经过语法分析得到的抽象语法树可以表示一个结构正确的源程序,但无法保证源程序的语义符合逻辑;语义分析则是对结构上源程序进行上下文相关性质进行检查(类型检测、控制流检查、数据流检查等);int a = 1;
boolean b = false;
char c = 2;
int d = a + c;
int d = b + c;
char d = a + c;
所有代码都可以构造正确的抽象语法树,但后两句在 Java 语言中是不符合逻辑的(语义分析异常,与具体的语言和上下文环境相关);
attribute
: 3.1,语义分析的标注检查;flow
: 3.2,语义分析的数据及控制流分析;desugar
: 3.3,解语法糖;generate
: 3.4. 生成字节码;a. 标注检查
常量折叠
(Constant Folding),javac 对源代码做的极少优化之一;int a = 1 + 2;
在抽象语法树仍然存在字面量 1
、2
和操作符 +
,但经过代码折叠,变量的值会被标记为 3
;因此在代码里定义 a=1+2
和 a=3
相比,并不会浪费哪怕一个处理器时钟周期的时间;
javac 的标记检查由 com.sun.tools.javac.comp.Attr
类和 com.sun.tools.javac.comp.Check
类实现;
b. 数据及控制流分析
对程序上下文逻辑进行进一步验证,检查如程序局部变量在使用前是否赋值、方法的每个路径是否都有返回值、是否所有受检异常都被正确处理等;与类加载时的数据及控制流分析的目的一直,但校验范围不同;
public void foo(final int arg){
final int var = 0;
// do something;
}
public void foo(int arg){
int var = 0;
// do something;
}
两种写法经过 javac 编译所得字节码完全一样,可见局部变量是否被 final 修饰对运行期是完全无影响的(不可知的),变量的不可变仅仅有 javac 编译器在编译期保障的;
javac 的数据及控制流分析由 com.sun.tools.javac.comp.Flow
类实现;
c. 解语法通
语法糖
,指计算机语言中的某种语法,其对语言的编译结果和功能不会有实际影响(JVM 不能支持这些语法,这些语法最终会被编译成基本语法结构),但却可以更方便编写者实用该语言(减少代码量、增加可读性、减少出错几率);如泛型(C# 的泛型是 CLR 支持的,不属于语法糖)、变长参数、自动装箱拆箱等;解语法糖
,将语法糖编译成原始基本语法结构;javac 的解语法糖由 com.sun.tools.javac.comp.TransTypes
类和 com.sun.tools.javac.comp.Lower
类实现;
d. 字节码生成
把语法树、符号表转发成字节码指令写到磁盘,并进行少量代码添加和转换工作;
代码添加
,如在语法树中添加实例构造器 ()
和类构造器 ()
;编译器会把语句块(()
的是{}
块,()
的是static {}
块)、变量初始化(实例变量和类变量)、调用父类的实例构造器(只有 ()
,()
中无须调用父类的 ()
,但经常会生成调用 java.lang.Object 的 ()
的代码)等操作收敛到 ()
和 ()
,并保障一定顺序执行(先父类实例构造器、再初始化变量、最后语句块);代码转换
,如将字符串的加操作替换为 StringBuilder 或 StringBuffer 的 append() 操作;javac 的字节码生成由 com.sun.tools.javac.jvm.Gen
类实现;将填充了所有信息的符号表输出到 Class 文件由 com.sun.tools.javac.jvm.CLassWriter
类实现;
上一篇:「JVM 原理使用」在远程服务端动态执行临时代码
下一篇:「JVM 编译优化」Java 语法糖(泛型、自动装箱/拆箱、条件编译)
PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!
参考资料: