在使用IDE开发时,run的时候会自动将.java
文件编译为.class
文件,如果是不依赖IDE,需要使用javac
命令,手动进行编译。那么javac
编译的过程发生了什么呢,如何将.java
文件进行编译的呢?
JVM的类加载的时候,加载的是字节码文件,在类加载之前,需要通过Javac编译器将
.java
文件编译为.class
文件,提供给JVM进行加载。
Java的编译期分为早期编译和晚期编译,早期编译指的是javac的编译过程,将源码转换为字节码,晚期编译指的是JIT编译过程,将字节码编译为机器码。
javac编译的过程
解析与填充符号表
- 解析
解析主要是词法分析、语法分析,词法分析会源代码中的关键字和代码变量等进行提取,放入集合中保存,语法分析主要是根据词法分析收集的集合中的关键字构建语法树,语法树即源码码文件的映射,在内存中的表示,语法树的每个节点包含包、类型、修饰符、运算符、返回值、代码注释等信息。构建语法树之后,就不会对源码文件进行操作,后续在内存中操作语法树即可 - 填充符号表
符号表示一个K-V类型的映射表,通过符号地址和符号信息构成,当对符号代表的变量进行地址分配时,通过符号表进行查找
并且会检查当前类是否有默认构造函数,如果当前类中没有任何的构造函数,那么在此阶段会提供一个默认的无参构造函数,但是目前该无参构造函数中并没有调用父类实例构造方法的代码块,该代码会在后续的字节码生成阶段填充
注解处理器
如果在源代码中使用注解,那么在编译的过程中,注解处理阶段会解析注解,并根据注解的功能进行处理,如果在注解处理时,会对上一步构建的语法树产生了修改(例如:动态代理生成的代理类、生成的get、set方法),那么就会重新回到解析与填充符号表阶段,重新构建语法树
语法分析与字节码生成
-
语法分析
语义分析主要是保证程序逻辑的正确性,对上下文进行校验。语义分析主要分为标注检查、数据及控制流分析- 标注检查
标注检查主要是检查变量是否在使用前声明、变量赋值的类型是否正确等
int a = 1 + 2
; 一般都会认为会增加CPU指令,直接定义为结果值较好,但是这样定义并不会增加运行期CPU指令的运算,因为在编译阶段就已经将常量的计算逻辑进行处理,进行常量折叠,结果为:int a = 3
- 数据及控制流分析
数据及控制流主要是对程序的上下文的逻辑进行分析是否正确,例如:方法是否有返回值,是否所有的编译时异常都抛出或者正确的try catch。
局部变量定义为
final
和普通的局部变量,在运行期(即生成字节码文件之后)是没有区别的,因为final
修饰的局部变量的不可变是由编译期来保证该变量不能够改变,和类变量和实例变量是不同的,因为类变量和实例变量是需要保存到常量池的,所以会携带到运行期。所以局部变量的不可变是在编译期的数据及控制流分析阶段进行保证,会检查该变量是否被修改,如果final
变量被修改,则编译报错- 解语法糖
java中的语法糖一般有:泛型、自动装箱/拆箱、变长参数、增强for循环、switch支持string、try-resource关闭资源等会在当前阶段进行编译,对语法糖进行分析,还原为真实的源代码
- 标注检查
-
字节码生成
字节码生成阶段主要是通过前端几个阶段校验和生成最终的语法树,将生成好的代码转换为字节码写入到磁盘中,生成字节码文件(.class
),并且还会添加一些额外的代码:- 生成类构造器
类构造器是通过编译阶段生成,并且是将代码中的所有的代码块、静态代码块、变量初始化(实例变量和静态变量)等操作收集到类构造器中,在类加载的初始化阶段调用。并且在此阶段,还会在实例构造器中加上调用父类实例构造器的代码。 - 代码优化
字节码生成阶段还会对代码做一些优化,保证生成的字节码运行更快,例如字符串的拼接,如果简单的对字符串进行拼接,因为String
类的不可变性,所以字符串拼接会生成新的对象,但是编译阶段会进行优化,将字符串拼接的操作替换为StringBuffer
或者StringBuilder
(如果jdk>1.5,则使用StringBuilder
)
- 生成类构造器