JAVA程序的编译过程

  1. 将 *.java文件转为 *.class的过程称为编译器的前端(前端编译)。例如:JDK的javac编译器。
  2. 把字节码( *.class文件) 转变为 本地机器码 的过程称为Java虚拟机的即时编译运行期(JIT编译器,Just In Time)。例如:HotSpot虚拟机的C1、C2编译器。
  3. 使用静态的提前编译器(AOT编译器,Ahead Of Time Compiler)直接把程序变异成与目标及其指令集相关的二进制代码的过程。例如:JDK的Jaotc。

一.前端编译与优化

Javac 这类编译器对代码的运行效率几乎没有任何优化措施,虚拟机设计团队把对性能的优化都放到了后端的即时编译器中,这样可以让那些不是由 Javac 产生的 class 文件(如 Groovy、Kotlin 等语言产生的 class 文件)也能享受到编译器优化带来的好处。但是 Javac 做了很多针对 Java 语言编码过程的优化措施来改善程序员的编码风格、提升编码效率。相当多新生的 Java 语法特性,都是靠编译器的「语法糖」来实现的,而不是依赖虚拟机的底层改进来支持。

Java 中即时编译器在运行期的优化过程对于程序运行来说更重要,而前端编译器在编译期的优化过程对于程序编码来说更加密切。

1.1 Javac 编译器

Javac 编译器的编译过程大致可分为 1个准备过程3个处理过程 :

  1. 初始化插入式注解处理器
  2. 解析与填充符号表;
  3. 插入式注解处理器的注解处理;
  4. 分析与字节码生成。

这 3 个步骤之间的关系如下图所示:

1.1.1 解析与填充符号表

? 解析步骤包含了经典程序编译原理中的词法分析和语法分析两个过程:

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

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

? 填充符号表

完成词法分析和语法分析之后,下一步就是填充符号表的过程。符号表是由一组符号地址和符号信息构成的表格。在语义分析中,符号表所登记的内容将用于语义检查和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。

1.1.2 注解处理器

**注解(Annotation)**是在 JDK 1.5 中新增的,注解在设计上原本是与普通代码一样,只在运行期间发挥作用。

但是在JDK1.6中,插入式注解处理器可以提前至编译期对代码中的特点注解进行处理,从而影响到前端编译器的工作过程。我们可以把插入式注解处理器看作是一组编译器的插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环过程称为一个轮次(Round),这也就对应着☝️ 图的那个回环过程有了编译器注解处理过程。Lombok就是依赖于插入式注解器实现的。

1.1.3 语义分析与字节码生成

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

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

在字节码生成阶段,除了生成构造器以外,还有一些其它的代码替换工作用于优化程序的实现逻辑,如把字符串的加操作替换为 StringBiulder 或 StringBuffer。

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

二. 后端编译与优化

目前主流的两款商用Java虚拟机(Hotspot、Open9)里,Java程序最初都是通过解释器(Interpreter)进行解释执行的。在javac编译过后产生的字节码Class文件:源码在编译的过程中,进行「词法分析 → 语法分析 → 生成目标代码」等过程,完成生成字节码文件的工作。然后在后面交由解释器)解释执行,省去前面预编译的开销。java.exe可以简单看成是Java解释器。

2.1 HotSpot 虚拟机内的即时编译器

当虚拟机发现某个方法或者代码块的运行特别频繁时,就会把这些代码认定为「热点代码」(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT)。

即时编译器不是虚拟机必须的部分,Java 虚拟机规范并没有规定虚拟机内部必须要有即时编译器存在,更没有限定或指导即时编译器应该如何实现。但是 JIT 编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键指标之一。

解释器与编译器

尽管并不是所有的 Java 虚拟机都采用解释器与编译器并存的架构,但许多主流的商用虚拟机,如 HotSpot、J9 等,都同时包含解释器与编译器。

**编译器:**负责把一种编程语言编写的源码转换成另外一种计算机代码,后者往往是以二进制的形式被称为目标代码(object code)。这个转换的过程通常的目的是生成可执行的程序。编译器,往往是在「执行」之前完成,产出是一种可执行或需要再编译或者解释的「代码」。

**解释器:**它直接执行由编程语言或脚本语言编写的代码,并不会把源代码预编译成机器码。它是把程序源代码一行一行的读懂然后执行,发生在运行时,产物是「运行结果」。

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

  • 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地机器码之后,可以获得更高的执行效率。
  • 当程序运行环境中内存资源限制较大(如部分嵌入式系统),可以使用解释器执行来节约内存,反之可以使用编译执行来提升效率。

同时,解释器还可以作为编译器激进优化时的一个「逃生门」,当编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,如加载了新的类后类型继承结构出现变化、出现「罕见陷阱」时可以通过逆优化退回到解释状态继续执行。

编译对象与触发条件

程序在运行过程中会被即时编译器编译的「热点代码」有两类:

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

这两种被多次重复执行的代码,称之为「热点代码」。


hhhhh