JVM笔记 - 程序编译与代码优化(早期(编译期)优化)

发布于 2015-03-01 | 更新于 2020-09-20

《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》笔记

1、概述

Java语言的“编译期”是一段不确定的操作过程,可能是:

  • 前端编译器(编译器的前端)把Java文件转换为class文件;Sun 的 Javac、 Eclipse   JDT 中的增量式编译器( ECJ)[ 1]。
  • 后端编译器(JIT编译期 Just in time compiler)把字节码变成机器码;JIT 编译器: HotSpot   VM 的 C1、 C2 编译器。
  • 静态编译器(AOT编译器 ahead of time compiler)直接把Java编译成本地机器代码;
  • AOT 编译器: GNU   Compiler   for   the   Java( GCJ)[ 2]、 Excelsior JET[ 3]。

本章讨论第一类编译过程。

Javac 这类编译器对代码的运行效率几乎没有任何优化措施(在 JDK   1. 3 之后, Javac 的- O 优化参数就不再有意义)。虚拟机设计团队把对性能的优化集中到了后端的即时编译器中。

2、Javac编译器

它本身就是一个由 Java 语言编写的程序,这为纯 Java 的程序员了解它的编译过程带来了很大的便利。

2.1、Javac的源码与调试

2.2、解析与填充符号表

2.3、注解处理器

提供了一组插入式注解处理器的标准 API 在编译期间对注解进行处理。

有了编译器注解处理的标准 API 后,我们的代码才有可能干涉编译器的行为,由于语法树中的任意元素,甚至包括代码注释都可以在插件之中访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。

在 Javac 源码中,插入式注解处理器的初始化过程是在 initPorcessAnnotations() 方法中完成的,而它的执行过程则是在processAnnotations() 方法中完成的。

2.4、语义分析与字节码生成

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

是否合乎语义逻辑必须限定在具体的语言与具体的上下文环境之中才有意义。

标注检查 Javac 的编译过程中,语义分析过程分为标注检查以及数据及控制流分析两个步骤。

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

由于编译期间进行了常量折叠,所以在代码里面定义" a= 1+ 2" 比起直接定义" a= 3", 并不会增加程序运行期哪怕仅仅一个 CPU 指令的运算量。

数据及控制流分析

数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。

将局部变量声明为 final, 对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障。局部变量与字段(实例变量、类变量)是有区别的,它在常量池中没有 CONSTANT_ Fieldref_ info 的符号引用,自然就没有访问标志( Access_ Flags) 的信息。

解语法糖

在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。

解语法糖的过程由 desugar() 方法触发。

字节码生成

字节码生成是 Javac 编译过程的最后一个阶段

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

还有其他的一些代码替换工作用于优化程序的实现逻辑,如把字符串的加操作替换为 StringBuffer 或 StringBuilder。

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

3、Java语法糖的味道

3.1、泛型与类型擦除

Java 语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型( Raw   Type, 也称为裸类型)了,并且在相应的地方插入了强制转型代码。

泛型擦除成相同的原生类型只是无法重载的其中一部分原因。

方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择,但是在 Class 文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说,两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个 Class 文件中的。

Signature、 LocalVariableTypeTable 等新的属性用于解决伴随泛型而来的参数类型的识别问题, Signature 是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名[ 3], 这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。

由于 List < String >和 List < Integer >擦除后是同一个类型,我们只能添加两个并不需要实际使用到的返回值才能完成重载。

擦除法所谓的擦除,仅仅是对方法的 Code 属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。

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

遍历循环则把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现 Iterable 接口的原因。

包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,以及它们 equals() 方法不处理数据转型的关系,笔者建议在实际编码中尽量避免这样使用自动装箱与拆箱。

3.3、条件编译

Java 语言当然也可以进行条件编译,方法就是使用条件为常量的 if 语句。

4、实战:插入式注解处理器

4.1、实战目标

4.2、代码实现

要通过注解处理器 API 实现一个编译器插件,首先需要了解这组 API 的一些基本知识。

在 JDK   1. 6 新增的 javax. lang. model 包中定义了 16 类 Element, 包括了 Java 代码中最常用的元素,如:“包( PACKAGE)、 枚举( ENUM)、 类( CLASS)、 注解( ANNOTATION_ TYPE)、 接口( INTERFACE)、 枚举值( ENUM_ CONSTANT)、 字段( FIELD)、 参数( PARAMETER)、 本地变量( LOCAL_ VARIABLE)、 异常( EXCEPTION_ PARAMETER)、 方法( METHOD)、 构造函数( CONSTRUCTOR)、 静态语句块( STATIC_ INIT, 即 static{} 块)、实例语句块( INSTANCE_ INIT, 即{}块)、参数化类型( TYPE_ PARAMETER, 既泛型尖括号内的类型)和未定义的其他语法树节点( OTHER)”。

4.3、运行与测试

4.4、其他应用案例

NameCheckProcessor 的实战例子只演示了 JSR- 269 嵌入式注解处理器 API 中的一部分功能,基于这组 API 支持的项目还有用于校验 Hibernate 标签使用正确性的 Hibernate   Validator   Annotation Processor[ 1]( 本质上与 NameCheckProcessor 所做的事情差不多)、自动为字段生成 getter 和 setter 方法的 Project   Lombok[ 2]( 根据已有元素生成新的语法树元素)等。

5、本章小结

之所以把 Javac 这类将 Java 代码转变为字节码的编译器称做“前端编译器”,是因为它只完成了从程序到抽象语法树或中间字节码的生成,而在此之后,还有一组内置于虚拟机内部的“后端编译器”完成了从字节码生成本地机器码的过程,即前面多次提到的即时编译器或 JIT 编译器,这个编译器的编译速度及编译结果的优劣,是衡量虚拟机性能一个很重要的指标。

Javac(Java在编译时)做了哪些事情

1、解析与填充符号表;
2、注解处理器;
3、语义分析与字节码生成:
3.1、标注检查
3.2、数据及控制流分析
3.3、解语法糖
3.3.1、泛型与类型擦除
3.3.2、自动装箱、拆箱与遍历循环
3.3.3、条件编译
3.4、字节码生成

后端编译器把字节码转换成本地机器码

本文作者: arthinking

本文链接: https://www.itzhai.comjvm-note-for-compile-optimize.html

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。

×
IT宅

关注公众号及时获取网站内容更新。