(八)早期(编译器)优化

早期(编译器)优化

本章主要内容:

  1. 从编译器源码实现的层次上了解JAVA源代码编译为字节码的过程
  2. 分析了JAVA语言中泛型、主动装箱/拆箱、条件编译等多种语法糖的前因后果
  3. 实战练习如何使用插入式注解处理器来完成一个检查程序命名规范的编译器插件

概述

java的有哪些“编译期”?前端编译器和后端编译器的职能?

java语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个
前端编译器(其实叫“编译器的前端”更准确点)把.java文件转化为.class文件的过程;
也可能是指虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转化为机器码的过程;
还可能是指使用静态提前编译器(AOT编译器,Ahead Of Time Compiler)直接把*.java文件编译成本地机器代码的过程。

1,前端编译器:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ);
2,JIT编译器:HotSpot VM的C1、C2编译器;
3,AOT编译器:GNU Compiler for the Java(GCJ)、Excelsior JET;

前端编译器和后端编译器的职能?

前端编译器对代码的运行效率几乎没有任何优化措施。虚拟机设计团队把对性能的优化集中到了后端的即时编译器中,这样可以让那些不是由Javac产生的Class文件(如JRuby、Groovy等语言的Class文件)也同样能享受到编译器优化所带来的好处。

前端处理器如javac,做了许多针对Java语言编码过程的优化措施来改善程序员的编码风格和提高编码效率。相当多新生的Java语法特性,都是靠编译器的“语法糖”来实现。

javac编译器

Javac为什么是由java写的?

用术语来描述,这便是一个语言的自举,自举的意思是说,一种编程语言的程序可以用这种语言本身编写的编译器来编译。

知乎上的轮子哥是这样简要地描述自举的过程的:

你想创造一门V语言而且用V语言来写V编译器的话,你得按照下面的方法做:
1、用C++把那个编译器(A)写出来,顺便留下很多测试用例。
2、用V语言把那个编译器写(B)出来,用A.exe来编译B,修改直到所有测试用例都通过为止。
3、B.exe来编译B自己得到B2.exe,修改直到B2.exe所有测试用例都通过为止。这是为了保证,就算B本身有很多bug,至少编译自己是没有bug的,从而你就可以走到第四步。
4、当你觉得有信心了,用A.exe把B编译一遍,就得到了B.exe。然后A的代码和A.exe都在也不需要存在了,删掉他们。以后你就不断的用B.exe来编译下一个版本的B就好了。就自举了。
作者:vczh
链接:https://www.zhihu.com/question/28513473/answer/41094452
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

完成自举有如下的优点:

  1. 这是一种对该语言的极好的测试;
  2. 编译器的开发者只需掌握这一门语言即可,不需要再掌握其它语言;
  3. 开发编译器的环境和使用这门语言开发的所有其它程序一致;
  4. 对编译器后端的优化不仅会优化以后所有编译出来的其它程序的效率,也会优化编译器本身的效率。

javac的过程?

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

javac的过程

javac的主体代码

解析与填充符号表

  1. 词法、语法分析

    词法分析是将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符都可以成为标记,如“int a = b + 2”这句代码包含了6个标记,分别是int、a、=、b、+、2,虽然关键字int由3个字符构成,但是它只是一个Token,不可再拆分。

    语法分析是根据Token序列构造抽象语法树的过程,抽象语法树(Abstract Syntax Tree,AST)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个阶段都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。

    在这之后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上。

  2. 填充符号表

    符号表(Symbol Table)是由一组符号地址和符号信息构成的表格,读者可以把它想象成哈希表中K-V值对的形式(实际上符号表不一定是哈希表实现,可以是有序号表、树状符号表、栈结构符号表等)。符号表中所登记的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的说明是否一致)和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。

注解处理器

注解处理器的类别和作用?

在JDK1.5之后,Java语言提供了对注解的支持,这些注解与普通的Java代码一样,是在运行期间发挥作用的。在JDK1.6中提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,我们可以把它看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语
法树进行修改为止,每一次循环称为一个Round。

有了编译器注解处理的标准API后,我们的代码才有可能干涉编译器的行为,由于语法树中的任意元素,甚至包括代码注释都可以在插件之中访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。只要有足够的创意,程序员可以使用插入式注解处理器来实现许多原本只能在编码中完成的事情,最后会给出一个使用插入式注解处理器的简单实战。

语义分析与字节码生成

语义分析的作用?

语义分析的主要任务是对语法树结构上正确的源程序进行上下文有关性质的审查,如进行类型审查。

  1. 标注检查:变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。
  2. 数据及控制流分析:对程序上下文逻辑更进一步的验证,如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等

通过第六章的讲解我们已经知道,局部变量与字段(实例变量、类变量)是有区别的,它在常量池中没有CONSTANT_Fieldref_info的符号引用,自然就没有访问标志(Access_Flags)的信息,甚至可能连名称都不会保留下来(取决于编译时的选项),自然在Class文件中不可能知道一个局部变量是不是声明为final了。因此,将局部变量声明为final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障。

解语法糖

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

字节码生成

该阶段的作用?

字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。例如,前面章节中多次提到的实例构造器()方法和类构造器()方法就是在这个阶段添加到语法树之中的。

JAVA语法糖的味道

语法糖的利与弊?

利:几乎各种语言或多或少都提供过一些语法糖来方便程序员的代码开发,这些语法糖虽然不会提供实质性的功能改进,但是它们或能提高效率,或能提升语法的严谨性,或能较少编码出错的机会。

弊:不过也有一种观点认为语法糖并不一定都是有益的,大量添加和使用“含糖”的语法,容易让程序员产生依赖,无法看清语法糖的躺椅背后,程序代码的真实面目,如泛型就是一个例子。

常见语法糖:

  1. 泛型
  2. 自动装箱、拆箱与遍历循环
  3. 条件编译

插入式注解处理器实战

本文标题:(八)早期(编译器)优化

文章作者:Sun

发布时间:2019年12月04日 - 15:12

最后更新:2019年12月04日 - 15:12

原始链接:https://sunyi720.github.io/2019/12/04/JVM/(八)早期(编译器)优化/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。