深入理解JVM(九)——早期(编译期)优化

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

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

解析与填充符号表过程

词法,语法分析

词法分析是将源代码的字符流转变成标记(Token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字,变量名,字面量,运算符都可以成为标记,如int a=b+2这段代码包含6个标记int,a,=,b,+,2,虽然关键字int由3个字符构成,但是他只是一个Token,不可拆分。在Javac的源码里,词法分析由com.sun.tools.javac.parser.Scanner类来实现。

语法分析是根据Token序列构造抽象语法树的过程。抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包,类型,修饰符,运算符,接口,返回值甚至代码注释等都可以是一个语法结构。在Javac的源码里,语法分析过程由com.sun.tools.javac.parser.Parser类来实现,这个阶段产生的抽象语法树由com.sun.tools.javac.tree.JCTree类表示,经过这个步骤之后,编译器基本不会再对源码进行操作了,后续操作都是建立在抽象语法树上。

填充符号表

完成词法分析和语法分析之后,就是填充符号表的过程。符号表(Symbol Table)是一组符号地址和符号信息构成的表格。符号表中所登记的信息在编译的不同阶段都要用到。
如在语义分析过程中,符号表中所登记的内容将用于语义检查和产生中间代码。在目标代码生成阶段,当对符号表进行地址分配时,符号表时地址分配的依据。
在Javac的源码里,填充符号表的过程是由com.sun.tools.javac.comp.Enter类来实现,此过程的出口是一个待处理列表(To Do List),包含了每一个编译单元的抽象语法树的顶级节点以及package-info.java的顶级节点。

注释处理器

JDK1.5之后,Java语言提供了对注解(Annotation)的支持,这些注解和普通代码一样,在运行期间发挥作用。

JDK1.6的规范中,提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,可以看做一组编译器的插件,在这些插件里面,可以读取,修改,添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每次循环称为一个Round。

在javac的源码中,插入式注解处理器的初始化过程是在initProcessAnnotations()方法中完成的,执行过程则是在processAnnotations()方法中完成的,这个方法判断是否还有新的注解处理器需要执行,如果有通过com.sun.tools.javac.processing.JavacProcessingEnvironment类的doProcessing()方法生成一个新的JavaCompiler对象对编译的后续步骤进行处理。

语义分析和字节码生成

语法分析后,编译器获得了程序代码的抽象语法树表示。语法树能表示一个结构正确的源代码抽象,但无法保证源代码时符合逻辑的。而语义分析则是对结构正确的源代码进行上下文有关性质的审查,如进行类型审查等。
Javac的编译过程,语义分析过程分为标注检查以及数据及控制流分析两个步骤,分别由attribute()和flow()方法完成。

标注检查

标准检查的步骤检查的内容包括如变量使用前是否已经被申明,变量与赋值之间的数据类型是否能够匹配等。在标注检查过程中还有一个主要的动作叫常量折叠
如int a = 1+2;
那么在语法树上看到字面量1,2,已经操作符+,但是经过常量折叠后,它们会折叠成常量3。由于编译期间进行了常量折叠,所以代码里面定义a=3并不会增加运行期哪怕一个CPU指令运算符。
在javac的源码中,标注检查的实现类是com.sun.tools.javac.comp.Attr和com.sun.tools.javac.comp.Check

数据及控制流分析

数据及控制流分析对程序的上下文逻辑进行更进一步的验证,如程序局部变量在使用前是否有赋值,方法的每天路径是否有返回值,是否所有的受查异常都被正确处理了等。
在javac的源码中,数据及控制流分析的入口是flow()方法,实现类是com.sun.tools.javac.comp.Flow类。

解语法糖

指在计算机语言中添加的某种语法,这种语法对语言功能并没有影响,但是更方便程序员使用。通常使用语法糖增加程序的可读性,从而减少程序代码出错的机会。

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

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

字节码生成

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

如实例构造器< init>()方法,类构造器< clinit>()方法就是在这个阶段添加到语法树中的。(这里的实例构造器并非默认构造函数,如果程序代码中没有提供构造函数,则编译器会添加一个没有参数的,访问性与当前类一致的默认构造器,这个过程在填充符号表阶段就已经完成了)。
这两个构造器的产生过程实际上是一个代码收敛的过程,编译器会把语句块(对于实例构造器而言是{}块,对于类构造器而言是static{}块),变量初始化(实例变量和类变量),调用父类的实例构造器(仅仅是实例构造器,< clinit>()方法中无须调用父类的< clinit>()方法,虚拟机会保证父类构造器的执行,但在< clinit>()方法中经常会生成调用java.lang.Object的< init>()方法的代码)等操作收敛到< init>()方法和< clinit>()方法之中,并且保证一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序。上述动作由由Gen.normalizeDefs()方法来实现。
除了生成构造器外还有一些代码替换的工作用于优化程序的实现逻辑,如把字符串的加操作替换成StringBuffer或StringBuilder的append操作。

完成对语法树的遍历和调整之后,就会填充所有需要信息的符号交给com.sun.tools.javac.jvm.ClassWriter类

泛型与类型擦除

本质是参数化类型的应用,将所操作的数据类型指定为一个参数,这种参数可以在类,接口和方法中创建,称为泛型类,泛型接口,泛型方法。

Java中的泛型和C#不同,它只存在与程序源码中。在编译后的字节码里面就已经替换成了原来的原生类型,并且在相应的地方插入了强制转换的代码,对Java而言,ArrayList< int>和ArrayList< String>就是同一类,泛型实际上是Java的一颗语法糖,Java中泛型实现称为类型擦除,即伪泛型。

猜你喜欢

转载自blog.csdn.net/rickey17/article/details/76326890
今日推荐