第10章和第11章 前端编译与优化 和 后端编译与优化

book:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》

文章目录

    • 10.1 概述
    • 10.2 Javac编译器
      • 10.2.1 Javac的源码与调试
      • 10.2.2 解析与填充符号表
        • 1.词法、语法分析
        • 2.填充符号表
      • 10.2.3 注解处理器
      • 10.2.4 语义分析与字节码生成
        • 1.标注检查
        • 2.数据及控制流分析
        • 3.解语法糖
        • 4.字节码生成
    • 10.3 Java语法糖的味道
      • 10.3.1 泛型
      • 10.3.2 自动装箱、拆箱与遍历循环
      • 10.3.3 条件编译
    • 10.4 实战:插入式注解处理器
    • 11.1 概述
    • 11.2 即时编译器
      • 11.2.1 解释器与编译器
      • 11.2.2 编译对象与触发条件
      • 11.2.3 编译过程
      • 11.2.4 实战:查看及分析即时编译结果
    • 11.3 提前编译器
      • 11.3.1 提前编译的优劣得失
      • 11.3.2 实战:Jaotc的提前编译
    • 11.4 编译器优化技术

10.1 概述

在Java技术下谈“编译期”而没有具体上下文语境的话,其实是一句很含糊的表述,下面列举了这3类编译过程里一些比较有代表性的编译器产品:

  • 前端编译器:把*.java文件转变成*.class文件的过程。
    • JDK的Javac、Eclipse JDT中的增量式编译器(ECJ)。
  • 即时编译器:运行期把字节码转变成本地机器码的过程。
    • HotSpot虚拟机的C1、C2编译器,Graal编译器。
  • 提前编译器:直接把程序编译成与目标机器指令集相关的二进制代码的过程。
    • JDK的Jaotc、GNU Compiler for the Java(GCJ)、Excelsior JET。

在本章后续的讨论里,笔者提到的全部“编译期”和“编译器”都仅限于第一类编译过程。

10.2 Javac编译器

10.2.1 Javac的源码与调试

从Javac代码的总体结构来看,编译过程大致可以分为1个准备过程和3个处理过程,它们分别如下所示。

  1. 准备过程:初始化插入式注解处理器。
  2. 解析与填充符号表过程,包括:
    • 词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树
    • 填充符号表。产生符号地址和符号信息。
  3. 插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段,本章的实战部分会设计一个插入式注解处理器来影响Javac的编译行为。
  4. 分析与字节码生成过程,包括:
    • 标注检查。对语法的静态信息进行检查。
    • 数据流及控制流分析。对程序动态运行过程进行检查。
    • 解语法糖。将简化代码编写的语法糖还原为原有的形式。
    • 字节码生成。将前面各个步骤所生成的信息转化成字节码。

上述3个处理过程里,执行插入式注解时又可能会产生新的符号,如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号,从总体来看,三者之间的关系与交互顺序如下图所示。
请添加图片描述

10.2.2 解析与填充符号表

1.词法、语法分析

词法分析是将源代码的字符流转变为标记(Token)集合的过程。单个字符是程序编写时的最小元素,但标记才是编译时的最小元素。关键字、变量名、字面量、运算符都可以作为标记。

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

2.填充符号表

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

10.2.3 注解处理器

JDK 5之后,Java语言提供了对注解(Annotations)的支持。

可以把插入式注解处理器看作是一组编译器的插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环过程称为一个轮次(Round),这也就对应着图10-4的那个回环过程。

10.2.4 语义分析与字节码生成

经过语法分析之后,编译器获得了程序代码的抽象语法树表示,抽象语法树能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的。语义分析的主要任务则是对结构上正确的源程序进行上下文相关性质的检查,譬如进行类型检查、控制流检查、数据流检查,等等。

1.标注检查

标注检查步骤要检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等等。

2.数据及控制流分析

数据流分析和控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。编译时期的数据及控制流分析与类加载时的数据及控制流分析的目的基本上可以看作是一致的,但校验范围会有所区别,有一些校验项只有在编译期或运行期才能进行。

3.解语法糖

语法糖(Syntactic Sugar),也称糖衣语法,指的是在计算机语言中添加的某种语法,这种语法对语言的编译结果和功能并没有实际影响,但是却能更方便程序员使用该语言。通常来说使用语法糖能够减少代码量、增加程序的可读性,从而减少程序代码出错的机会。
Java中最常见的语法糖包括了泛型、变长参数、自动装箱拆箱等等,Java虚拟机运行时并不直接支持这些语法,它们在编译阶段被还原回原始的基础语法结构,这个过程就称为解语法糖。

4.字节码生成

字节码生成是Javac编译过程的最后一个阶段,字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码指令写到磁盘中,编译器还进行了少量的代码添加和转换工作。

10.3 Java语法糖的味道

10.3.1 泛型

泛型的本质是参数化类型(Parameterized Type)或者参数化多态(Parametric Poly morphism)的应用,即可以将操作的数据类型指定为方法签名中的一种特殊参数,这种参数类型能够在类、接口和方法的创建中,分别构成泛型类、泛型接口和泛型方法。泛型让程序员能够针对泛化的数据类型编写相同的算法,这极大地增强了编程语言的类型系统及抽象能力。

Java选择的泛型实现方式叫作“类型擦除式泛型”(Type Erasure Generics),而C#选择的泛型实现方式是“具现化式泛型”(Reified Generics)。

Java的类型擦除式泛型无论在使用效果上还是运行效率上,几乎是全面落后于C#的具现化式泛型,而它的唯一优势是在于实现这种泛型的影响范围上:擦除式泛型的实现几乎只需要在Javac编译器上做出改进即可,不需要改动字节码、不需要改动Java虚拟机,也保证了以前没有使用泛型的库可以直接运行在Java 5.0之上。

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

装箱、拆箱基本概念:

  • 装箱:基本类型转变为包装器类型的过程。
  • 拆箱:包装器类型转变为基本类型的过程。

装箱和拆箱的执行过程:

  • 装箱是通过调用包装器类的 valueOf 方法实现的。
  • 拆箱是通过调用包装器类的 xxxValue 方法实现的,xxx代表对应的基本数据类型。

如int装箱的时候自动调用Integer的valueOf(int)方法;Integer拆箱的时候自动调用Integer的intValue方法。

注意:

  • 包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,包装类的equals()方法不处理数据转型的关系。

10.3.3 条件编译

意义不大,建议掠过。

10.4 实战:插入式注解处理器

请看原书。

11.1 概述

无论是提前编译器抑或即时编译器,都不是Java虚拟机必需的组成部分,《Java虚拟机规范》中从来没有规定过虚拟机内部必须要包含这些编译器,更没有限定或指导这些编译器应该如何去实现。但是,后端编译器编译性能的好坏、代码优化质量的高低却是衡量一款商用虚拟机优秀与否的关键指标之一,它们也是商业Java虚拟机中的核心,是最能体现技术水平与价值的功能。

11.2 即时编译器

目前主流的两款商用Java虚拟机(HotSpot、OpenJ9)里,Java程序最初都是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器。

11.2.1 解释器与编译器

解释器与编译器两者各有优势:

  • 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即运行。
  • 当程序启动后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,这样可以减少解释器的中间损耗,获得更高的执行效率。

HotSpot虚拟机中内置了两个(或三个)即时编译器,其中有两个编译器存在已久,分别被称为“客户端编译器”(Client Compiler)和“服务端编译器”(Server Compiler),或者简称为C1编译器和C2编译器(部分资料和JDK源码中C2也叫Opto编译器),第三个是在JDK 10时才出现的、长期目标是代替C2的Graal编译器

分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包括:

  • 第0层。程序纯解释执行,并且解释器不开启性能监控功能(Profiling)。
  • 第1层。使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能。
  • 第2层。仍然使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。
  • 第3层。仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。
  • 第4层。使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。

11.2.2 编译对象与触发条件

上面提到了在运行过程中会被即时编译器编译的目标是“热点代码”,这里所指的热点代
码主要有两类,包括:

  • 被多次调用的方法。
  • 被多次执行的循环体。

要知道某段代码是不是热点代码,是不是需要触发即时编译,这个行为称为“热点探测”(Hot Spot Code Detection),其实进行热点探测并不一定要知道方法具体被调用了多少次,目前主流的热点探测判定方式有两种,分别是:

  • 基于采样的热点探测。采用这种方法的虚拟机会周期性地检查各个线程的调用栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”。
  • 基于计数器的热点探测。采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。

这两种探测手段在商用Java虚拟机中都有使用到,譬如J9用过第一种采样热点探测,而在HotSpot虚拟机中使用的是第二种基于计数器的热点探测方法,为了实现热点计数,HotSpot为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter,“回边”的意思就是指在循环边界往回跳转)。当虚拟机运行参数确定的前提下,这两个计数器都有一个明确的阈值,计数器阈值一旦溢出,就会触发即时编译。

方法调用计数器触发即时编译的编译条件如下图所示:
第10章和第11章 前端编译与优化 和 后端编译与优化_第1张图片
第10章和第11章 前端编译与优化 和 后端编译与优化_第2张图片

11.2.3 编译过程

有需要,请看原书。

11.2.4 实战:查看及分析即时编译结果

有需要,请看原书。

11.3 提前编译器

11.3.1 提前编译的优劣得失

现在提前编译产品和对其的研究有着两条明显的分支:

  • 一条分支是做与传统C、C++编译器类似的,在程序运行之前把程序代码编译成机器码的静态翻译工作。
  • 另外一条分支是把原本即时编译器在运行时要做的编译工作提前做好并保存下来,下次运行到这些代码(譬如公共库代码在被同一台机器其他Java进程使用)时直接把它加载进来使用。

11.3.2 实战:Jaotc的提前编译

有需要,请看原书。

11.4 编译器优化技术

四项有代表性的优化技术:

  • 方法内联
  • 逃逸分析
  • 公共子表达式消除
  • 数组边界检查消除

你可能感兴趣的:(#,前端,jvm,java)