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;

版权声明:本文为qq_44891295原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_44891295/article/details/115225144