Javac编译过程

Javac编译过程

  Javac编译过程大致分为4个过程,分别是:

  1. 词法分析
  2. 语法分析
  3. 语义分析
  4. 代码生成

词法分析

  词法分析是将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符都可以成为编辑,如“int a+b=2”这句代码中包含了6个标记,分别是int、a、=、b、+、2,虽然关键字int由三个字符构成,但是它只是一个Token,不可再拆分。在Javac的源码中,词法分析过程由com.sun.tools.javac.parser.Scanner类来实现。


语法分析

  词法分析器的作用是将Java源文件的字符流转变成对应的Token流。而语法分析器是将词法分析器分的Token流组件成更加结构化的语法树,也就是将一个个单词组装成一句话,一个完整的语句。哪些词语组合在一起是主语,哪些是谓语、哪些是宾语、哪些是定语等没要做进一步区分。

  语法分析是根据Token序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。语法分析过程由com.sun.tools.javac.parser.Parser类实现,这个阶段产出的抽象语法树由com.sun.tools.javc.tree.JCTree类表示,经过这个步骤之后,编译器就基本不会再对源码文件进行操作了,后续的操作都是建立在抽象语法树上。


语义分析

  语法分析之后,编译器获得了程序代码的抽象语法树表示,语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。语义分析是要在语法树的基础上再做一些处理,如给类添加默认的构造函数,检查变量在使用前是否已经初始化,将一些常量进行合并处理,检查操作变量类型是否匹配,检查所有的操作语句是否可达,检查checked exception是否正确处理。

  语义分析阶段分为:填充符号表、标注检查、数据及控制流分析。

填充符号表

  符号表是由一组符号地址和符号信息构成的表格,读者可以把它想象成哈希表K-V值对的形式。符号表中所登记的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检测和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。在Javac源码中,填充符号表的过程由com.sun.tools.javac.comp.Enter类实现。

  一个类除了类本身会定义一些符号变量如类名称、变量名称和方法名称等,还有一些符号是引用其它类的,这些符号会调用其它类的方法或者变量等,还有一些类可能会继承或者实现超类和接口等。这些符号都是在其他类中定义的,那么就需要将这些类的符号也解析到符号表中。

  在Enter类解析这一步骤中,还有一个重要的步骤就是添加默认的构造函数。如果代码中没有提供任何构造函数,那么编译器将会添加一个没有参数、访问下与当前一致的默认构造函数。

标注检查

  检查的内容包括诸如变量的类型是否匹配、变量在使用前是否已经初始化、能够推导出泛型方法的参数类型、字符串常量的合并(常量折叠)。在标注检查步骤中一个重要的动作称为常量折叠,如果我们在代码中写了如下定义:

int a=1+2;

  那么在语法树上仍然能看到字面量1、2以及操作符+,但是在进过常量折叠之后,他们将会被折叠为字面量3.实现的类是com.sun.tools.javac.comp.Attr类和com.sun.tools.javac.comp.Check类。

数据流分析

  数据流主要完成如下工作:

  • 检查变量在使用前是否都已经被正确赋值。
  • 保证final修饰的变量不会被重复赋值。
  • 要确定方法的返回值类型。这里需要检查方法的返回值类型是否确定,并检查接受这个方法返回值的引用类型是否匹配,如果没有返回值,则不能有任何引用类型指向方法的这个返回值。
  • 所有的Checked Exception都要捕获或者向上抛出。
  • 所有的语句都要被执行到。这里会检查是否有语句出现在一个return方法的后面,因为在return方法后面的语句永远也不会被执行到。

控制流分析

  控制流主要完成如下工作:

  • 去掉无用的代码,比如永假的if代码块。
  • 变量的自动转换,比如自动装箱拆箱。
  • 去除语法糖。解语法糖的过程由desugar()方法触发,在com.sun.tools.javac.comp.TransTypes和com.sun.tools.javac.comp.Lower类中完成。

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


字节码生成

  由com.sun.tools.javac.jvm.Gen类来完成。字节码阶段不仅仅把前面各个步骤所生成的信息(语法树、符号表)转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。

  实例构造器<init>方法和类构造器<clinit>方法就是在这个阶段添加到语法树中的。

  生成java字节码需要经过以下两个步骤:

  • 将java方法中的代码块转化成符合JVM语法的命令形式,JVM的操作都是基于栈的,所有的操作都必须经过出栈和进栈来完成。
  • 按照JVM的文件组织格式将字节码输出到以class为扩展名的文件中。

  在jdk1.5之后,java语言提供了对注解(Annotation)的支持,这些注解与普通的Java代码一样,是在运行期间发挥作用的。在Jdk1.6中提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,我们可以把它看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。对注解的处理是在填充符号表之后及在标注注解之前发生的。


参考

  1. 《深入理解Java虚拟机》周志明著。
  2. 《深入分析Java Web技术内幕》许令波著。

欢迎支持笔者的作品《深入理解Kafka: 核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客(ID: hiddenkafka)。
本文作者: 朱小厮

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×