Java中的编译分为两个部分:
源码文件编译成字节码文件(前端编译)
字节码文件被虚拟机加载以后编译成机器码(后端编译)
对于开发来说接触的一般都是第一个步骤也就是源码编译成字节码文件(class文件),第二个步骤开发几乎不会接触,因为这是虚拟机在运行过程中自己做的一些编译流程,将字节码转换成可被虚拟机识别执行的机器码。
1. 前端编译
前端编译大致主要有以下流程:
对源文件进行词法分析产生字符流
对字符流进行语法分析产生抽象语法树
对语法树进行语义分析,确保语义正常
语义分析通过以后生成中间代码(字节码)
下面我们站在javac的角度上看,编译过程大致分为:
解析与填充符号表
插入式注解处理器的处理注解
语义分析与字节码的生成
上述3个过程会包含前段编译4个步骤的所有流程,下面我们看一下这个3个步骤需要做什么。
2. 解析与填充符号表
2.1 解析
2.1.1 词法分析
Java源文件是由一个个字符构成,但是编译器所能识别的是Token(标记)。我们需要通过词法分析来将源文件中的字符流转换成Token集合,这样才能用于后续的语法分析。
int a = b + c;
int是由3个字符构成,但是对于词法分析来说,这三个字符会被解析成一个Token(标记)。
词法分析主要由com.sun.tools.javac.parser.Scannaer类来实现。
2.1.2 语法分析
根据Token集合生成抽象语法树,语法树是一种用来表示程序代码语法结构的表现形式,语法树的每一个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符。
package jvm;
/**
* @author sh
*/
public class ClassTest{
public int add(int a, int b){
return a + b;
}
}
语法分析主要有com.sun.tools.javac.parser.Parser类来实现。
上述这段代码生成的抽象语法树如下(IDEA JDT AstView插件可以查看抽象语法树):
上述抽象语法树在Java中使用com.sun.tools.javac.tree.JCTree类来表示,之后所有的操作均建立在抽象语法树之上。
2.2 填充符号表
结束了词法分析和语法分析以后,下一步就是填充符号表。符号表中信息可以用在语义分析过程中的检查和产生中间代码
3. 注解处理器
注解处理器在编译期间对注解进行处理,可以读取、修改、添加抽象语法树中的任意元素。如果注解处理器对语法树进行了修改,编译器将会回到解析和填充符号表的过程重新处理,直到注解处理器不再对语法树进行修改为止,每一次循环称为一个Round。
4. 语义分析和字节码生成
4.1 语义分析
语义分析主要是对程序上下文进行检查,如变量类型检查。
语义分析主要包含两个步骤:
标注检查
数据及控制流分析
4.1.1 标注检查
标注检查主要用来检查变量是否已被声明、变量与赋值之间的数据类型是否匹配。在标注检查的步骤中还会实施常量折叠。
int a = 1 + 2;
上述代码我们在语法树上可以看到字面量1、2以及操作符+,在经过常量折叠步骤以后,会生成一个新的字面量3,在程序运行时a的值就是3,不会再消耗CPU进行计算。
4.1.2 数据及控制流分析
数据及控制流分析是对程序上下文逻辑进行验证,检查局部变量是否在使用前已经赋值、方法的每条路径都有返回值、所有的受检查异常是否被正确处理。
局部变量final类型的变量的不变性只能依靠编译来保证,这是因为局部变量在常量池中没有CONSTANT_Fieldref_info的符号引用,没有访问标志的信息,在运行期虚拟机并不确定局部变量是否是final,因此需要编译期的保证。
4.2 字节码生成
字节码在生成之前,还需要进行最后一项工作解语法糖。
4.2.1 解语法糖
Java中的语法糖包括范型、变长参数、自动装箱/拆箱、Lambda。
语法糖可以增加程序的可读性、减少代码量。
4.2.2 字节码生成
字节码生成是javac编译的最后一个阶段。字节码生成阶段不仅仅是把各个步骤生成的信息转换成字节码写到磁盘,还进行了代码的添加和转换工作。
将static语句块、static变量收敛到
方法中
将实例变量初始化、调用父类构造器收敛到
方法
程序优化,比如将字符串的+操作替换成StringBuilder的append
完成了语法树的遍历和调整以后,就会填充了所有信息的符号表交给com.sun.tools.javac.jvm.ClassWriter类,最后由该类的writeClass()方法输出字节码。
本期的Java前端编译介绍到这,我们下期再见!!!
我是shysh95,希望可以和你专注技术的路上并肩作战,长按识别或者扫码关注微信公众号,更多精彩文章!!!