JDT中的增量式编译器(ECJ),相当多新生的Java语

作者:美狮美高梅官方网站

说起Java语言的编译期,它可能是指编译器把Java源码文件转变为Class字节码文件的过程,也可能是指虚拟机在运行时把字节码转变为机器代码的过程(JIT编译器,Just In Time Compiler)。本章我们来讨论一下上面提到的第一类编译过程

早期(编译期)优化

前端编译器(把*.java文件转变成*.class文件):Sun的Javac、 Eclipse JDT中的增量式编译器(ECJ)。
JIT编译器(把字节码转变成机器码):HotSpot VM的C1、 C2编译器。
AOT编译器(把*.java文件编译成本地机器代码):GNU Compiler for the Java(GCJ)、 Excelsior JET。

本文基于周志明的《深入理解java虚拟机 JVM高级特性与最佳实践》所写。特此推荐。

Javac编译器

我们都直接或间接的使用过Javac编译器,它可以将Java源码文件编译为Class字节码文件。Javac做了许多针对Java语言编码过程的优化措施来提升编码风格和编码效率。Java中很多新的语法特性,并非直接靠虚拟机底层直接支持,而是通过编译器的语法糖来实现

Javac的编译过程大致分为3个过程:解析与填充符号表、插入式注解处理器的注解处理过程、分析与字节码生成

解析

解析包含词法分析语法分析两个过程

词法分析是将源代码中的字符流转变为Token集合的过程。Token是编程过程中的最小元素,比如“int a = b + 1”这行代码一共包含了6个Token,分别是int、a、=、b、+、1

语法分析是根据Token序列构造抽象语法树(Abstract Syntax Tree,AST)的过程。抽象语法树是一种描述程序语法结构的树形表示形式。后续的操作都是建立在抽象语法树之上

填充符号表

完成了解析之后,下一步就是填充符号表。符号表是一组符号地址和符号信息构成的表格。符号表中所记录的信息在编译的不同阶段都要用到。在语义分析中,符号表将用于语义检查和产生中间代码。在目标代码生成阶段,符号表将是对符号名进行地址分配的依据

在JDK1.5之后,Java语言提供了对注解的支持。在JDK1.6中,提供了一组插入式注解处理器的API。这组API可以在编译期读取、修改、添加抽象语法树中的任意元素。在此期间,如果抽象语法树被修改,编译器将重新回到解析与填充符号表的过程重新处理,直到抽象语法树没有再被修改为止。这组API的意义就在于,通过它可以以编程的方式干涉编译器的行为

图片 1

语法分析的结果,使编译器获得了抽象语法树。抽象语法树能够表示一个结构正确的程序抽象,但却无法保证程序符合逻辑。语义分析的作用就是结合上下文,对程序的逻辑进行审查

标注检查

标注检查涉及的内容如:变量使用前是否被声明、给变量赋值的数据类型是否匹配等。其中有一个重要的动作称为常量折叠,比如有如下代码:

int a = 1 + 2;

经过常量折叠,与之等效的代码如下:

int a = 3;

也就是说,对于类似上述两段代码,在运行时效率是相同的。原因在于编译期已经进行过常量折叠

数据及控制流分析

数据及控制流分析涉及的内容如:局部变量使用前是否被赋值、方法的每条路径是否都有返回值、是否所有的Check Exception都被正确处理等。编译期的数据及控制流分析与类加载(关于类类加载方面的内容,请参考本系列文章:类加载机制)时的数据及控制流分析的目的基本是一致的,但是对于特定的校验项只能在编译期或者加载时期进行

比如下面这两个方法,区别在于方法参数及方法体内局部变量是否被final修饰:

图片 2

但是在编译后,两个方法却是一样的,final修饰符被去除:

图片 3

原因在于:类变量(实例变量、静态变量)在常量池中有CONSTANT_Field_info符号引用(关于类文件结构方面的内容,请参考本系列文章:类文件结构),而局部变量没有,自然也就没有访问标志信息(access_flags),因此也就不会有变量不变性的信息。也就是说,变量不变性仅在编译期保证

解语法糖

语法糖,是一种方便程序员使用,但是对功能没有影响的语法。Java中许多新的特性并非通过修改虚拟机底层来支持,而是通过语法糖来实现。比如:泛型、变长参数、自动装箱、拆箱等。这些特性在编译阶段被还原成简单的基础语法结构,这个过程就叫做解语法糖。关于语法糖的内容,后面再做介绍

字节码生成

字节码生成是Javac编译过程的最后一个阶段,但这个阶段并不仅仅是把前面各步骤生成的信息转化为字节码并写入磁盘,编译器在此阶段还进行了少量的代码添加和转换工作

比如:实例构造器<init>()方法和类实例构造器<cinit>()方法就是在这个阶段被添加到语法树中的(这里的实例构造器并不是默认构造方法,如果程序中没有提供任何构造方法,那么编译器会在填充符号表阶段添加一个默认构造方法)。这两个构造器会将调用父类的实例构造器代码、变量的初始化代码、语句块(“{}”块和“static {}”块)代码按此顺序进行收敛

除此之外,还会进行一些代码的优化工作,比如将字符串的加操作替换为StringBuilder.append()等。之后生成最终的Class文件,至此整个编译过程完成

比如源码如下:

图片 4

编译后:

图片 5

Javac编译器

编译过程大致分为三个:解析与填充符号表过程、插入式注解处理器的注解处理过程、分析与字节码生成过程。

  • 解析与填充符号表过程
    • 1.词法、 语法分析
      • 词法分析是将源代码的字符流转变为标记(Token)集合
      • 语法分析是根据Token序列构造抽象语法树的过程,抽象语法树(Abstract Syntax Tree,AST)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct)
        ![](https://upload-images.jianshu.io/upload_images/4986428-b3055597bcc5309c.png)

        图1 Javac编译过程

-   2.填充符号表
    -   符号表(Symbol Table)是由一组符号地址和符号信息构成的表格
  • 注解处理器
    • 如果插入式注解处理器在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,如图1的回环过程(Round)。
  • 语义分析与字节码生成
    • 语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,如进行类型检查。
    • 语义分析过程分为标注检查以及数据及控制流分析
      • 1.标注检查
        • 检查的内容包括诸如变量使用前是否已被声明、 变量与赋值之间的数据类型是否能够匹配等
      • 2.数据及控制流分析
        • 对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、 方法的每条路径是否都有返回值、 是否所有的受查异常都被正确处理了等问题。
  • 局部变量与字段(实例变量、 类变量)是有区别的,它在常量池中没有
    CONSTANT_Fieldref_info的符号引用,自然就没有访问标志(Access_Flags)的信息,甚至可能连名称都不会保留下来(取决于编译时的选项),自然在Class文件中不可能知道一个局部变量是不是声明为final
    • 3.解语法糖
      • 语法糖指指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。
      • 虚拟机运行时不支持泛型、变长参数等语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。
    • 4.字节码生成
      • 字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、 符号表)转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。
    • 实例构造器<init>()方法和类构造器<clinit>()方法就是在这个阶段添加到语法树之中的

列举了这3类编译过程中一些比较有代表性的编译器

语法糖

前面提到过,语法糖,是一种方便程序员使用,但是对功能没有影响的语法。Java中许多新的特性并非通过修改虚拟机来做底层支持,而是通过语法糖来实现。下面来举例说明:

在一些编程语言中,泛型在源码以及编译后都是切实存在的,List<int>与List<String>就是两个不同的类型,这种称为真实泛型

但是在Java中,泛型仅存在于源码,经过编译后,泛型会被擦除(被替换为原生类型,并且在相应的地方插入了强制类型转换),这种称为伪泛型

比如源码如下:

图片 6

编译后通过javap查看其反汇编代码:

图片 7

红色框中创建map,但是并没有泛型信息,说明泛型被擦除

蓝色框中调用map.put()方法,参数类型都是Object,并没有泛型信息,说明泛型被擦除

绿色框中调用map.get()方法,参数及返回类型都是Object,并没有泛型信息,说明泛型被擦除。checkcast指令检查是否可进行类型转换

黄色框中,通过LocalVariableTypeTable、Signature属性记录原始的泛型信息,但这并不等于泛型信息没有被擦除(关于LocalVariableTypeTable和Signature的信息,可查看Java Virtual Machine Specification)。所谓擦除,仅仅是对方法Code属性中的字节码进行擦除,但是在元数据中依然保留了泛型信息

比如源码如下:

图片 8

编译后通过javap查看其反汇编代码:

图片 9

红色框中创建一个长度为3的Integer类型数组,用于Arrays.asList方法的参数,说明可变参数实际是通过数组来实现的

蓝色框中分别将3个int类型参数转换为Integer,用于存入上一步创建的Integer类型数组,这就是自动装箱

绿色框中通过迭代器对list进行遍历,说明foreach循环是通过迭代器来实现的。这也是为什么foreach循环遍历的对象要求实现Iterable接口

黄色框中将每次遍历的Integer类型数据转换为int类型,这就是自动拆箱

除了上面介绍的这些,Java还有不少其他语法糖,如内部类、枚举、断言、针对枚举和字符串的switch语句、try语句中定义和关闭资源等。这些均可通过javap命令了解其本质

思维导图:

图片 10

笔记6结束

Java语法糖

  • 前端编译器:Sun的Javac、 Eclipse JDT中的增量式编译器( ECJ ) 。
  • JIT编译器:HotSpotVM的C1、C2编译器。
  • AOT编译器: GNU Compiler for the Java ( GCJ ) 、 Excelsior JET。

泛型与类型擦除

泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

相当多新生的Java语法特性,都是靠编译器的“语法糖”来实现,而不是依赖虚拟机的底层改进来支持,前端编译器在编译期的优化过程对于程序编码来说关系更加密切。

自动装箱、 拆箱与遍历循环

Javac编译器

Javac的源码存放在JDK_SRC_HOME/langtools/src/share/classes/com/sun/tools/javac中, 除了JDK自身的API外 ,就只引用了JDK_SRC_HOME/langtools/src/share/classes/com/sun/*里面的代码 ,调试环境建立起来简单方便,因为基本上不需要处理依赖关系。

从Sun Javac的代码来看,编译过程大致可以分为3个过程,分别是:

  • 解析与填充符号表的过程。
  • 插入式注解处理器的注解处理过程。
  • 分析与字节码生成过程。

这3个步骤之间的关系与交互顺序如下图

图片 11

Javac的编译过程

Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类 ,上述3个过程的代码逻辑集中在这个类的compile() 和compile2() 方法中,其中主体代码如下图所示,整个编译最关键的处理就由图中标注的8个方法来完成,下面我们具体看一下这8个方法实现了什么功能。

图片 12

Javac编译过程的主体代码

条件编译

解析与填充符号表

解析步骤由图10-5中的parseFiles()方法(图10-5中的过程1.1 ) 完成,解析步骤包括了经典程序编译原理中的词法分析和语法分析两个过程。

本文由美狮美高梅官方网站发布,转载请注明来源

关键词: