Java 的编译过程
- 前端编译: 编译器的前端,将 Java 文件转变成 Class 文件的过程;如 JDK 的 javac、Eclipse JDT 中的增量式编译器 ECJ;
- 即使编译: JIT,Just In Time Compiler,在运行期将字节码转变成本地机器码的过程;如 HotSpot VM 的 C1、C2 编译器,Graal 编译器;
- 提前编译: AOT,Ahead Of Time Compiler,直接静态将程序编译成目标机器指令集的二进制代码的过程;如 JDK 的 Jaotc、GNU Compiler for the Java(GCJ)、Excelsior JET;
即使编译器在运行期的优化支持了程序执行效率的提升,而前端编译器在编译期的优化支持了程序员的编码效率和语言使用幸福感的提高;
文章目录
- 1. javac 的源码与调试
- 2. 解析与填充符号表
- 3. 注解处理器
- 4. 语义分析与字节码生成
1. javac 的源码与调试
-
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 个处理过程;
准备过程
: 初始化插入式注解处理器;解析与填充符号表
过程,包括:
a. 词法、语法分析;将源代码的字符流转变为标记集合,构造出抽象语法树;
b. 填充符号表;产生符号地址和符号信息;插入式注解处理器的注解处理
过程: 插入式注解处理器的执行阶段,影响 javac 的编译行为;分析与字节码生成
过程,包括:
a. 标注检查;对语法的静态信息进行检查;
b. 数据流及控制流分析;对程序动态运行过程进行检查;
c. 解语法糖;将简化代码编写的语法糖还原为原有的形式;
d. 字节码生成;将前面各个步骤所生成的信息转化成字节码;
插入式注解可能会产生新的符号,如果有新的符号产生,就必须转回解析、填充符号表的过程重新处理新的符号;
javac 编译入口代码在 com.sun.tools.javac.main.JavaCompiler
类的 compile() 方法;
2. 解析与填充符号表
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. 填充符号表
- 符号表(Symbol Table),一组符号地址和符号信息构成的数据结构(包含每个编译单元的抽象语法树的顶级节点和 package-info.java 的顶级节点),类似于 Hash 表的键值对存储结构(也可以是有序符号表、树状符号表、栈结构符号表等形式);符号表在语义分析阶段用于语义检查和产生中间代码,在目标代码生成阶段用于地址分配;javac 中填充符号表由
com.sun.tools.javac.comp.Enter
类实现;
3. 注解处理器
Java 在 JDK 5 开始支持注解(Annotations),原只对程序运行期间发挥作用;到 JDK 6 时添加了插入式注解处理器
的标准 API,提前至编译期处理特定注解,可以影响前端编译器的工作过程;
插入式注解处理器相当于编译器的插件,通过这些插件可以读取、修改、添加抽象语法树的任意元素;若插件在处理注解期间对抽象语法树进行修改,编译器将回退至解析和填充符号表的过程,直到插入式注解处理器不再修改抽象语法树;每一次循环称为一个轮次(Round);
编码效率工具 Lombok 通过注解实现自动生成 getter/setter 方法、空置检查、生成受查异常表、生成 equals() 和 hashCode() 等功能,都是依赖插入式注解处理器实现的;
initProcessAnnotations
: 准备过程,初始化插入式注解处理器;processAnnotations
: 完成插入式注解执行处理;若有新的注解处理器需要执行,则通过com.sun.tools.javac.processing.JavacProcessingEnvironment
类的doProcessing()
生成一个新的JavaCompiler
对象,进行后续的编译已处理;
4. 语义分析与字节码生成
语义分析
,经过语法分析得到的抽象语法树可以表示一个结构正确的源程序,但无法保证源程序的语义符合逻辑;语义分析则是对结构上源程序进行上下文相关性质进行检查(类型检测、控制流检查、数据流检查等);
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. 字节码生成
把语法树、符号表转发成字节码指令写到磁盘,并进行少量代码添加和转换工作;
代码添加
,如在语法树中添加实例构造器<init>()
和类构造器<clinit>()
;编译器会把语句块(<init>()
的是{}
块,<clinit>()
的是static {}
块)、变量初始化(实例变量和类变量)、调用父类的实例构造器(只有<init>()
,<clinit>()
中无须调用父类的<clinit>()
,但经常会生成调用 java.lang.Object 的<init>()
的代码)等操作收敛到<init>()
和<clinit>()
,并保障一定顺序执行(先父类实例构造器、再初始化变量、最后语句块);代码转换
,如将字符串的加操作替换为 StringBuilder 或 StringBuffer 的 append() 操作;
javac 的字节码生成由 com.sun.tools.javac.jvm.Gen
类实现;将填充了所有信息的符号表输出到 Class 文件由 com.sun.tools.javac.jvm.CLassWriter
类实现;
上一篇:「JVM 原理使用」在远程服务端动态执行临时代码
PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!
参考资料:
- [1]《深入理解 Java 虚拟机》