javac编译器-语义分析与字节码生成

语法分析之后,编译器获得了程序代码的抽象语法树表示,语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的,而语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,如进行类型审查

Javac的编译过程中,语义分析过程分为标注检查以及数据及控制流分析两个步骤,分别对应attribute()和flow()方法

  • 标注检查

    标注检查步骤检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。在标注检查步骤中,还有一个重要的动作称为常量折叠,如定义:

1
    int a = 1 + 2

那么在语法树上仍然能看到字面量”1”、”2”以及操作符”+”,但是经过常量折叠之后,它们将会被折叠为字面量”3”。由于编译期间进行了常量折叠,所以在代码中的定义”a=1+2”比起直接定义”a=3”,并不会增加程序运行期哪怕仅仅一个CPU指令的运算量

标注检查步骤在Javac源码中的实现类是com.sun.tools.javac.comp.Attr类和com.sun.tools.javac.comp.Check类

  • 数据及控制流分析

    数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检测出诸如程序局部变量是在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。编译时期的数据及控制流分析与类加载时数据及控制流分析的目的基本上是一致的,但校验范围有所区别,有一些校验只有在编译期或运行期才能进行。下面举一个关于 final 修饰符的数据及控制流分析的例子,见代码清单 10-1。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    // 方法一带有 final 修饰  
    public void foo(final int arg) {  
        final int var = 0;  
        // do something  
    }  
    
    // 方法而没有 final 修饰  
    public void foo(int arg) {  
        int var = 0;  
        // do something  
    }

在这两个 foo() 方法中,第一种方法的参数和局部变量定义使用了 final 修饰符,而第二种方法则没有,在代码编写时程序肯定会受到 final 修饰符的影响,不能再改吧 arg 和 var 变量的值,但是这两段代码编译出来的 Class 文件是没有任何一点区别的,通过第 6 章的讲解我们已经知道,局部变量与字段(实例变量、类变量)是有区别的,它在常量池中没有 CONSTANT_Fieldref_info 的符号引用,自然就没有访问标志(Access_Flags)的信息,甚至可能连名称都不会保留下来(取决于编译时的选项),自然在 Class 文件中不可能知道一个局部变量是不是声明为 final 了。

因此,将局部变量声明为 final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障。

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

  • 解语法糖

    语法糖(Syntactic Sugar),也称糖衣语法,指在计算机语言中添加的某种语法,使用语法糖能够增加程序的可读性,从而减少代码出错的机会

    Java中最常用的语法糖主要是泛型、变长参数、自动装箱/拆箱等,虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖

    在Javac的源码中,解语法糖的过程由desugar()方法触发,在com.sun.tools.javac.comp.TransTypes类和com.sun.tools.javac.comp.Lower类中完成

  • 字节码生成

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

    完成了对语法树的遍历和调整之后,就会把填充了所有所需信息的符号表交给com.sun.tools.javac.jvm.ClassWriter类,由这个类的writeClass()方法输出字节码,生成最终的Class文件,到此整个编译过程宣告结束