【JVM】javac的编译过程

Page content

Java 编译是先把xx.java文件编译成xx.class文件。xx.class是个中间状态的字节码(Byte Code)。
类加载器(Class Loader)读取到JVM里后, 再一次解析成机器码(Binary Code)执行。
也就是说Java是需要做两次编译,其中的第一次编译(.java => .class)是如何执行的呢?

之前介绍过C语言编译执行是需要4个步骤(预处理,编译,汇编,链接)。
(详细的可以参考之前写的【C语言的编译和执行过程】)
那javac的编译是如何进行的呢?

javac的编译(.java => .class)过程大体上可以分3个步骤。

  1. 解析填充符号表Parse and Enter
  2. 插入式注解处理器的注解处理过程Annotation Processing
  3. 分析与字节码生产过程Analyse and Generate

这三个步骤之间的关系和交互顺序如下图所示:
可以看到如果注解处理器在处理注解期间对语法树进行了修改,编译器将回到解析和填充符号表的过程进行重新处理。
直到注解处理器没有再对语法树进行修改为止。(不太明白也没关系,下面有详细的解释。)

图片备用地址
javac_compile

下面详细的介绍一下每个过程是如何进行的。

1. 解析填充符号表Parse and Enter

首先会做词法分析,语法分析以及填充符号表。

1.1 词法分析

将源代码的字符流转变为最小单元的标记(Token)集合。
比如如“int a = b + 1”这句代码包含了 int、a、=、b、+、1 => 6个标记。

1.2 语法分析

这里就对token流做语法分析了。比如if后面接的是不是返回值为bool的表达式之类的。
然后把符合规范的语法构造抽象语法树(AST,Abstract Syntax Tree)。

1.3 填充符号表

符号表是有一组符号地址和符号信息构成的表格。
填充符号表的过程的出口是一个待处理列表,包含了每一个抽象语法树(和package-info.java)的顶级节点。

2. 插入式注解处理器的注解处理过程Annotation Processing

注解处理器可以理解为抽象语法树的一组插件,这些插件可以对抽象语法树直接进行读取,修改,添加操作。
如果在解析注解期间,对语法树进行了修改,那么编译器回到解析及填充符号表的过程重新处理,
直到所有的插入式注解处理器没有对语法树进行修改为止。

有了编译器注解处理的标准API后,我们的代码才有可能干涉编译器的行为,
由于语法树中的任意元素,甚至包括代码注释都可以在插件之中访问到,
所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。
只要有足够的创意,程序员可以使用插入式注解处理器来实现许多原本只能在编码中完成的事情。

3. 分析与字节码生产过程Analyse and Generate

这部分看源代码的话(可以参考下面我截图的源代码compile2函数)可以分:标注检查,数据和控制流分析,解析语法糖以及生成字节码。
很多时候把标注检查,数据和控制流分析,解析语法糖合起来说语义分析。

3.1 语义分析

语法分析后可以保证形成语法树以后不存在语法错误,但无法保证源程序是符合逻辑,所以需要对源程序上下文进行审查。
还有Java会有一些相对复杂的语法,语义分析的作用就是将这些复杂的语法翻译成更简单的语法。

3.1.1 标注检查:
包括变量使用前是否已声明, 变量与赋值之间的数据类型是否匹配, 常量折叠:int a = 1 + 2 ==> int a = 3

3.1.2 数据流分析和控制流分析:
数据流及控制流的分析入口是flow()方法,具体操作由com.sun.tools.javac.comp.Flow类来完成。

数据流分析
局部变量是否赋值,final修饰的变量不会被重复赋值,方法路径返回值验证,受检异常的正确处理,所有的语句是否都要被执行等等…

控制流分析
去掉无用的代码,比如永假的if代码块,变量的自动转换,比如自动装箱拆箱等等…

3.1.3 解语法糖:
泛型,装箱拆箱,for循环,条件编译等等等…
解语法糖的过程由desugar()方法触发,在com.sun.tools.javac.comp.TransTypes和com.sun.tools.javac.comp.Lower类中完成。

3.2 字节码生成:

字节码生成是 javac 编译过程的最后一个阶段。
这一阶段还进行少量的代码添加和转换工作。
比如实例构造器方法和类构造器方法就是在这个阶段添加到语法树之中。
这里的实例构造器并不是指默认的构造函数,而是指我们自己重载的构造函数。
完成了对语法树的遍历和调整之后,就会把填充了所有所需信息的符号表交给com.sun.tools.javac.jvm.ClassWriter类,
由这个类的writeClass()方法输出字节码,生成最终的class文件。

源代码

百闻不如一见,上面啰里啰嗦的说了一大堆不如看javac的源代码理解的会更快,
Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类,
上述3个过程的代码逻辑集中在这个类的compile()和compile2()方法中,

图片备用地址
javac_compile

总结

按照顺序我们可以这么简单的理解javac的编译过程。

  1. 词法分析 => 把源代码转变为token
  2. 语法分析 => 使用token生成抽象语法树
  3. 语义分析 => 做一些逻辑上的验证,优化语法树
  4. 生成字节码 => 调整完语法树后生成最终class文件。

题外话: class文件

从.java源文件辛辛苦苦编译成.class文件。那.class文件的结构又是如何呢?
先写一个经典HelloWorld程序:

public class ClassTest {
    public static void main(String[] args) {
        System.out.println("hello world!");
    }
}

使用javac命令编译以后,用 vi 命令查看一下里面的内容

图片备用地址
javac_class

看不明白…^^;;
好像只能看出来初始化引用包以及我写的类名和大致的内容。
(用javap -v 命令可以查看详细内容。) 可以用IDEA直接打开.class文件查看里面的内容,会看的很舒服。

图片备用地址
javac_class

里面具体的结构就去查其他资料吧…^^;;
还没研究过这里面的东西 哈~


欢迎大家的意见和交流

email: li_mingxie@163.com