文章目录
javac这种将源代码转化为字节码的过程在编译原理上属于前端编译,不涉及相关代码的生成和优化。
JDK中的javac本身是用Java语言编写的,在某种意义上实现javac语言自举。javac没有使用类似的YACC和Lex这样的生成器工具,所有词法分析和语法分析等功能都是自己实现,代码精简高效
通过以下学习,我们可以知道javac编译过程的七个阶段和各阶段作用
1:javac的七个阶段
1 ) parse:读取java源文件,做词法分析(LEXER)和语法分析(PARSER)
2 ) enter: 生成符号表
3 ) process: 处理注解
4) attr: 检查语义合法性、常量折叠
5) flow: 数据流分析
6) desugar:去除语法糖
7 ) generate:生成字节码
接下来我们逐一介绍各项内容。
1.1 第一阶段:parse
parse阶段的主要作用是读取.java源文件并做词法分析和语法分析
词法分析( lexical analyze)将源代码拆分为一个个词法记号(Token),这个过程又被称为扫描( scan),比如代码i=2 +2在词法分析时会被拆分为五部分: i、=、2、+、2。这个过程会将空格、空行、注释等对程序执行没有意义的部分排除。词法分析与我们理解英语的过程类似,比如英语句子“i am studying”在我们大脑中会被拆分为i、am、studying三个单词。
javac中的词法分析由com.sun.tools.javac.parser.Scanner 实现,以语句“intk=i+j;”
Scanner会读取源文件中的内容,将其解析为Java语言的Token序列,这个过程如下图所示。
词法分析之后是进行语法分析( syntax analying),语法分析是在词法分析的基础上分乐单间之间的关系,将其转换为计算机易于理解的形式,生成抽象语法树( Abttct SyntaxTree,AST)。AST是一个树状结构,树的每个节点都是一个语法单元,抽象语法树是后续的语义分析、语法校验、代码生成的基础。与其他多数语言一样,javac也是使用递归下降法来生成抽象语法树。如下图
解析CSV,JSON,XML本质也是语法分析和一部分语义分析的过程
1.2 第二阶段:enter
enter阶段的主要作用是解析和填充符号表(symbol table),主要由com.suntools.javac.comp.Enter和com.sun tools javac.comp.MemberEnter类来实现。符号表是由标识符、标识符类型、作用域等信息构成的记录表。在遍历抽象语法树遇到类型、变量、方法定义时,会将它们的信息存储到符号表中,方便后续进行快速查询。
int x=5;//定义int型字段,初始化为5
public long add(long a,long b){
return a+b;
}
javac使用Symbol类来表示符号,每一个符号都包含名称,类别和类型这三个关键属性。如下
name:表示符号名,如上的x,add都是符号名
kind:表示符号类别,上面的x符号类别是Kinds.VAR,表示这是一个变量符号,add的符号类型是Kinds,MTH,表示这是一个方法符号
type:表示符号类型,上面的x的符号类型是int,add方法的符号类型是null
Symbol类是一个抽象类,常见的有VarSymbol,MethodSymbol
Symbol定义了符号是什么,作用域则指定了符号的有效范围,由com.sun tools javac.code.Scope,如下
public void test(){
int x=0;
}
public void test1(){
int x=0;
}
test函数和test1函数都定义了一个名为x的int变量,这两个变量可以独立使用,在超出各自的方法体作用域后就对外不可见,外部也访问不到
注意:enter阶段除了上述生成符号表,还会在类文件中没有默认构造方法的情况下,添加< init >构造方法等
1.3 第三阶段:process
process用来做注解的处理,这个步骤由com.sun.tools.javac processing JavacProcessing-Environment类完成。**从JDK6开始,javac 支持在编译阶段允许用户自定义处理注解,大名鼎鼎的lombok框架就是利用了这个特性,通过注解处理的方式生成目标class 文件,比在运行时反射调用性能明显提升。**会在以后单独进行介绍,这里不再展开。
1.4 第四阶段:attr
attrt 阶段是语义分析的一部分, 主要由com.sun.tools.javac comp.Attrt类实现,这个阶段会做语义合法性检查、常量折叠等,由com.sun.tools.javac.comp 包下的Check、Resolve、ConstFold、Infer 几个类辅助实现。
- com. sun. tools. javac. comp.Check类的主要作用是检查变量类型、方法返回值类型是否合法,是否有重复的变量、类定义等,比如下面这些场景。
1 )检查方法返回值是否与方法声明的返回值类型一致,以下面的代码为例。
public int foo() {
return "hello";
}
这个检查由com.sun.tosjavac comp.Check类的checkType 完成,这个方法的定义如下。
Type checkType(final DiagnosticPosition pos, final Type found, final Type req,final CheckContext checkContext){
当抽象语法树遍历到return “hello”;对应的节点时,得到的found参数值为对象类型String,req类型为int,返回值类型和方法声明不一致
2)检查是否有重复的定义,比如检查同一个类中是否存在相同的签名方法。校验逻辑由Check类的checkUnique方法实现。以下面的代码为例。
public void test()
public void test(
在编译时会出现重复定义的错误.
- com.sun. tools.javac .comp.Resolve类的主要作用是:
检查变量、方法、类访问是否合法,比如pivate 方法的访问是否在方法所在类中访问等。
为重载方法调用选择最具体的方法。
以下面的代码为例:
publie static void method(objecet obj) {
System. out.print1n( "method # object");
}
public static void method(String obj) {
System.out.printIn("method # String");
}
public static void main(String[] args) {
method(null);
}
在Java中允许方法重载( overload),但要求方法签名不能一样。 调用method(null)实示是调用第二个方法输出“method # String”, javac 在编译时会推断出最具体的方法,方法的选择在Resolve 类的mostSpecific方法中完成。第二个方法的入参类型String 是第一个方法的入参类型Object的子类,会被javac认为更具体。
- com.sun.tools.javac. comp.ConstFold类的主要作用是在编译期将可以合并的常量合并,比如常量字符串相加,常量整数运算等,以下面的代码为例。
public void test() {
int x=1+2;
String y = "hel" + "lo";
int z=100 / 2;
对应的字节码如下所示:
0: iconst 3
1: istore 1
2: ldc #2 // String hello
4: astore_ 2
5: bipush 50
7: istore_ 3
8: return
可以看到编译后的字节码已经将常量进行了合并,javac 没有理由把这些可以在编译期完成的计算留到运行时。
- com.sun.tools javac.comp.Infer类的主要作用是推导泛型方法的参数类型,比较简单,这里不再赘述。
1.4 第五阶段:flow
阶段主要用来处理数据流分析。主要由com.sun.tools.javac.comp.Flow类实现很多编译期的校验在这个阶段完成,
下面列举几常见的场景。
1)检查非void方法是否所有的分支都有返回值,以下面的代码为例:
public boolean test(int x) (
if(x==0)
return true
/注释掉这个return
// returnfalse;
上面的代码编译报错
2)检查受检异常(checked eception)是否被捕获或者显式抛出,以下面的代码为例。
public void test() {
throw new FileNotFoundException();
}
上面的代码编译报错
3)检查局部变量使用前是否被初始化。Java中的成员变量在未赋值的情况下会赋值为默认值,但是局部变量不会,在使用前必须先赋值,以下面的代码为例。
public void test() {
int x;
inty=x+1;
System. out.println(y);
}
上面的代码编译报错如下:
error: variable x might not have been initialized
inty=x+1;
4)检查final变量是否有重复赋值,保证fnal 的语义,以下面的代码为例。
public void test(final int x) {
x=1;
System. out.println(x);
}
上面的代码编译报错
1.5 第六阶段:desugar
下面某种意义上来说都算是语法糖:泛型,内部类,foreach语句,原始类型和包装类型之间的隐式转换,字符串和枚举的switch-case实现,后缀运算符(i++和++i)、变长参数等。
desugar的过程就是解除语法糖,主要由com.sun.tools.javac comp.TransTypes类和com.sun.tools. javac .comp.Lower类完成。TransTypes类用来擦除泛型和插入相应的类型转换代码,Lower类用来处理除泛型以外其他的语法糖。
下面列举几个场景
1)在desugar阶段泛型会被擦除,在有需要时自动为原始类和包装类型转化添加拆箱,装箱代码
以下代码示例
public void test() {
List<Long> idList = new ArrayList<>();
idList.add(1L);
long firstId =idList.get(0);
}
对应的字节码如下所示:
//执行new ArrayList<>()
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList. "<init>":()V
7: astore 1
8: aload 1
//把原始类型1自动装箱为Long类型
9: lconst_ 1
10: invokestatic #4 // Method java/ lang/Long.value0f: (J)Ljava/lang/Long;
//执行add调用
13: invokeinterface #5,2 // InterfaceMethod java/util/List. add: (Ljava/lang/object; )Z
18: pop
19: aload 1
// 执行get(0)调用
20: iconst _0
21: invokeinterface #6,2// InterfaceMethod java/uti1/List.get:(I)Ljava/lang/object;
//检查object 对象是否是Long类型
26: checkcast #7 // class java/1ang/Long
// 自动拆箱为原始类型
29: invokevirtual #8 // Method java/ang/Long, longVaue()J
32: lstore. _2
33:return
2)去除逻辑死代码,也就是不可能进入的代码块,例子省略
3)String类,枚举类的switch-case也是在desugar阶段进行的。例子省略
1.6 第七阶段:generate
generate阶段的主要作用是遍历抽象语法树生成最终的Class文件,由com.sun.tools.javac.jvm.Gen类实现
1)初始化块代码并收集到< init>和< clinit >中,以下面的代码为例。
public class MyInit {
{
System.out. println( "hel1o");
}
public int a = 100;
生成的构造器方法对应的字节码如下所示。
public MyInit();
0: aload_0
1: invokespecial #1// Method java/lang/object. "<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String hello
9: invokevirtual #4 // Method java/ io/PrintStream. print1n: (Ljava/lang/String; )V
12: aload 0
13: bipush 100
15: putfield #5
18: return
可以看到,编译器会自动帮忙生成一个构造器方法< init >,没有写在构造器方法中的字段初始化、初始化代码块都被收集到了构造器方法中,翻译为Java源代码如下所示。
public class MyInit {
public int a;
public MyInit() {
System. out . println("hello");
this.a = 100;
}
与static修饰的静态初始化的逻辑一样,javac 会将静态初始化代码块和静态变量初始化收集到< clinit >方法中。
2)把字符串拼接语句转换为StringBuilder.append 的方式来实现,比如下面的字符串x和y的拼接代码。
public void foo(String x,String y) {
String ret=x+y;
System. out . println(ret);
}
在generate阶段会被转换为下面的代码。
public void foo(String x!string y) {
String ret = newstringBuilder().appenda(s).append(s2).tostring();system.out.printIn(ret);
}
3)为synchronized关键字生成异常表,保证monitorenter,monitorexit指令可以成对调用。
4)switch-case实现tableswitch和lookupswitch指令的选择,根据稀疏程度来选择
如下是在com.sun.tools.javac.jvm.Gen.java的源代码摘取的两个指令逻辑选择的一部分
// Determine whether to issue a tableswitch or a lookupswitch
// instruction.
long table_space_cost = 4 + ((long) hi - lo + 1); //hi代表case值的最大值,lo表示case值的最小值
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;// nlables等于case个数
long lookup_time_cost = nlabels;
int opcode =
nlabels > 0 &&
table_space_cost + 3 * table_time_cost <=
lookup_space_cost + 3 * lookup_time_cost
?
tableswitch : lookupswitch;