执行引擎
前情提要
非常推荐学完这篇文章后去看看美团技术专家的文章:Java 即时编译器原理解析及实践
# 执行引擎概述
- 执行引擎是 Java 虚拟机核心的组成部分之一。
- “虚拟机” 是一个相对于 “物理机” 的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
- JVM 的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被 JVM 所识别的字节码指令、符号表,以及其他辅助信息。
- 那么,如果想要让一个 Java 程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释 / 编译为对应平台上的本地机器指令才可以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。
图中的编译为:前端编译。
图中的执行为:解释执行和编译执行。
- 前端编译:将 Java 源代码文件(.java 文件)编译成 Java 字节码文件(.class 文件)。
- 解释执行:是指 JVM 启动时,逐行解释字节码指令,并将其转换为本地机器指令执行。这种方式的优点是跨平台,缺点是效率低。
- 编译执行(后端编译):是指 JVM 在运行时,将经常执行的字节码指令编译为本地机器指令,缓存起来,以提高执行效率。这种方式的优点是性能高,缺点是耗费内存。
# 前端编译(编译器的前端)
这是最常见的编译过程,涉及将 Java 源代码文件(.java 文件)编译成 Java 字节码文件(.class 文件)。这一过程主要由 Java 编译器(如 javac)完成。编译器的前端会进行词法分析、语法分析、语义分析以及生成中间表示代码等步骤。这个阶段是 Java 开发过程中最基础和必要的编译过程。
- 词法分析:这一步通常被称为分词。编译器将源代码的字符序列分割成一系列的记号(Token)。每个 Token 都有对应的类型和值。
- 语法分析:在这个阶段,Token 被用来生成一个解析树,也称为抽象语法树(AST)。AST 描述了每个 Token 节点之间的关系。
- 语义分析:这一步检查解析树是否符合所用编程语言的语义。这包括静态语义(在编译器可以确定的语义)和动态语义(只能在运行期才能确定的语义)。
- 中间代码生成:这一步将整个语法树转化为中间代码(Intermediate Code)。中间代码是与目标机器和运行环境无关的,使得编译器被分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。
# 后端编译
虚拟机的后端运行期编译器(JIT 编译器,Just In Time Compiler):JIT 编译器是 Java 虚拟机(JVM)的一部分,它在程序运行时将字节码转换成机器码。这一过程是动态的,即在 Java 程序执行时才进行。JIT 编译器通过即时编译技术优化程序的执行效率,通过将热点代码(即执行频率高的代码)编译成优化后的机器码来提升运行速度。JIT 编译器可以根据代码的运行情况动态优化编译策略,从而提高程序的性能。
静态提前编译器(AOT 编译器,Ahead of Time Compiler):AOT 编译是另一种编译策略,它允许直接将 Java 源代码或字节码编译成本地机器代码,这个过程在程序运行之前就完成了。与 JIT 编译相比,AOT 编译可以减少程序启动时间,提高程序运行效率,因为它避免了运行时编译的开销。AOT 编译特别适用于那些对启动速度有严格要求的应用场景。不过,AOT 编译也有其限制,如可能无法针对特定的硬件配置进行优化等。
# 工作过程
执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于 PC 寄存器。
每当执行完一项指令操作后,PC 寄存器就会更新下一条需要被执行的指令地址。
当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在 Java 堆区中的对象实例信息以及通过对象头中的元数据指针定位到目标对象的类型信息。
# Java 代码编译和执行过程
大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过图中的各个步骤。


# 什么是解释器?什么是 JIT 编译器?
- 解释器:当 Java 虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容 “翻译” 为对应平台的本地机器指令执行。这种方式的优点是跨平台,缺点是效率低。
- JIT 编译器:就是虚拟机在运行时,将经常执行的字节码指令编译为本地机器指令,缓存起来,以提高执行效率。这种方式的优点是性能高,缺点是占用内存和编译时间。
注意
JIT 编译器并不是将源代码一次性直接编译成机器语言,而是将字节码动态地编译成机器语言,并且只编译那些热点代码,也就是被频繁调用或循环的代码。
# 为什么 Java 是半编译半解释型语言?
- JDK1.0 时代,将 Java 语言定位为 “解释执行” 还是比较准确的。再后来,Java 也发展出可以直接生成本地代码的编译器。
- 编译阶段:Java 源代码(.java 文件)会被 Java 编译器(javac)编译成字节码文件(.class 文件),这些字节码文件是与平台无关的,可以在不同的系统上运行。
- 解释阶段:Java 虚拟机(JVM)会加载字节码文件,并通过执行引擎(Execution Engine)将其解释或编译成本地机器指令,交由 CPU 执行。
# 机器码、指令、汇编、高级语言理解与执行过程
# 机器码
- 各种用二进制编码方式表示的指令,叫做机器指令码。开始,人们就用它采编写程序,这就是机器语言。
- 机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用它编程容易出差错。
- 用它编写的程序一经输入计算机,CPU 直接读取运行,因此和其他语言编的程序相比,执行速度最快。
- 机器指令与 CPU 紧密相关,所以不同种类的 CPU 所对应的机器指令也就不同。
# 指令
- 由于机器码是由 0 和 1 组成的二进制序列,可读性实在太差,于是人们发明了指令。
- 指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令(一般为英文简写,如 mov,inc 等),可读性稍好。
- 由于不同的硬件平台,执行同一个操作,对应的机器码可能不同,所以不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同。
# 指令集
不同的硬件平台,各自支持的指令,是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集。如常见的
- x86 指令集,对应的是 x86 架构的平台。
- ARM 指令集,对应的是 ARM 架构的平台。
# 汇编语言
- 由于指令的可读性还是太差,于是人们又发明了汇编语言。
- 在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址。
- 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。
- 由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译(汇编)成机器指令码,计算机才能识别和执行。
# 高级语言
- 为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言。高级语言比机器语言、汇编语言更接近人的语言。
- 当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序或编译程序。
# 字节码
- 字节码是一种中间状态(中间码)的二进制代码(文件),它比机器码更抽象,需要直译器转译后才能成为机器码。
- 字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。
- 字节码的实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译为可以直接执行的指令。
- 字节码典型的应用为:Java bytecode。
# C、C++ 源程序执行过程
编译过程又可以分成两个阶段:编译和汇编。
编译:编译器将源代码进行词法分析、语法分析、语义分析、优化等操作,生成一种中间代码(如汇编语言),这种中间代码是与机器无关的,可以在不同的平台上使用。
汇编:汇编器将中间代码转换为目标代码,即机器语言,这种目标代码是与机器相关的,只能在特定的平台上运行。
# 解释器(Interpreter)
# 为什么要有解释器?
- JVM 设计者们的初衷仅仅只是单纯地为了满足 Java 程序实现跨平台特性,因此避免采用静态编译的方式由高级语言直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法(也就是产生了一个中间产品字节码)。
- 解释器真正意义上所承担的角色就是一个运行时 “翻译者”,将字节码文件中的内容 “翻译” 为对应平台的本地机器指令执行。
- 当一条字节码指令被解释执行完成后,接着再根据 PC 寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
提示
针对上面所述部分特性的解读。
- 跨平台特性的需求:Java 的设计目标之一是实现程序的跨平台性,即一次编写,到处运行(Write Once, Run Anywhere)。为了达到这个目标,需要一种机制来克服不同平台(如 Windows, Linux, macOS 等)之间在硬件和操作系统层面的差异。
- 静态编译与动态解释:通常,程序语言可以通过静态编译的方式将高级语言代码直接编译成特定平台的本地机器指令执行。然而,这种方法会使得程序与特定平台紧密绑定,从而丧失跨平台的能力。为了解决这个问题,JVM 的设计者们选择了另一条路径,即通过解释器在运行时动态地将中间形式(字节码)转换为本地机器指令。
- 字节码的引入:Java 源代码首先被编译成字节码(.class 文件),这是一种中间表示形式,既不是高级语言代码也不是特定平台的机器指令。字节码的设计使得它可以在任何实现了 JVM 的平台上运行,因为 JVM 负责将字节码翻译成能够在该平台上执行的本地机器指令。
- 解释器的作用:解释器在 JVM 中充当“翻译者”的角色,它逐条读取字节码指令,然后将它们转换(或解释)为对应平台的本地机器指令。这个过程是动态进行的,即在程序运行时实时完成。解释执行的过程涉及到读取指令(由 PC 寄存器指示下一条执行的字节码指令)、解释指令并执行相应的操作。
- 动态执行与性能考虑:虽然解释执行允许 Java 程序实现跨平台运行,但这种方式可能会牺牲执行效率,因为每次运行程序时都需要进行字节码到机器指令的转换。为了解决这个问题,JVM 采用了各种优化技术,如即时编译(JIT)技术,它会将热点代码(频繁执行的代码)编译成本地机器指令,以提高执行效率。
# 解释器的分类
在 Java 的发展历史里,一共有两套解释执行器,即古老的字节码解释器、现在普遍使用的模板解释器。
# 古老的字节码解释器
最初的字节码解释器采用了一种较为直接的方法来执行 Java 字节码:它通过软件代码来模拟字节码的执行过程。这种方法的直接性意味着它可以比较容易地实现和理解。然而,由于每一条字节码的执行都需要通过多个软件层次的解释和转换,这导致了相对较低的执行效率。在这个模型中,CPU 需要花费大量时间在解释字节码上,而不是直接执行与字节码等价的机器指令。
# 模板解释器
随着对执行效率的进一步追求,Java 虚拟机(JVM)的开发者引入了模板解释器。这种解释器的核心思想是将每一条字节码与一个模板函数相关联。每个模板函数内部包含了执行对应字节码所需的机器码。当解释器遇到一条特定的字节码时,它就会调用与之对应的模板函数,从而直接执行预先定义好的机器码。这种方法大大减少了字节码到机器指令的转换时间,从而提高了解释执行的效率。
# HotSpot VM 中的解释器
在 HotSpot VM 中,解释器主要由两个模块组成:Interpreter 模块和 Code 模块。
- Interpreter 模块:这个模块实现了解释器的核心功能,即按顺序执行字节码指令的逻辑。它负责读取字节码,决定执行哪个模板函数,并处理字节码指令之间的控制流。
- Code 模块:这个模块用于管理在运行时生成的本地机器指令。这包括模板函数中的机器码,以及即时编译器(JIT 编译器)生成的本地代码。Code 模块确保了机器指令的有效组织和优化执行。
# 解释器的现状
- 由于解释器在设计和实现上非常简单,因此除了 Java 语言之外,还有许多高级语言同样也是基于解释器执行的,比如 Python、Perl、Ruby 等。但是在今天,基于解释器执行已经沦落为低效的代名词,并且时常被一些 C/C++ 程序员所调侃。
- 为了解决这个问题,JVM 平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。
- 不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献。
# JIT 编译器
# Java 代码执行的分类
# 第一种:解释执行
- 字节码到机器码:运行时,JVM 的解释器逐条将这些字节码翻译成机器码并执行。这个过程发生在 JVM 内部,解释器直接读取字节码,逐条转换成机器指令,然后由 CPU 执行。这种方式的优点是可以快速启动程序,但执行速度较慢,因为每次运行都需要进行字节码到机器码的转换。
# 第二种:编译执行(JIT 编译)
- 即时编译技术(JIT):这种方式仍然需要源代码被编译成字节码。然而,在程序运行时,JVM 的 JIT 编译器会将这些字节码编译成与平台相关的机器码。这不是逐条解释执行,而是将整个方法或热点代码块(即频繁执行的代码)编译成机器码。
- 优化和执行:编译成机器码的过程中,JIT 编译器可以进行各种优化,如内联、去虚拟化等,以提高代码执行效率。一旦编译完成,编译后的代码可以直接由 CPU 执行,无需再经过解释器。这种方式虽然在程序初次运行时会有一些延迟(因为需要时间编译),但一旦编译完成,执行速度会非常快。
提示
要说明的是不要被说明文字所迷惑,上面的两种说的都是 JVM 种编译器的工作范畴,不要把解释执行理解为解释器的工作,解释执行只是说明将.class 转成机器码的过程。解释器的工作则是一行一行将.class 文件翻译成机器码。当然目前 java 中的代码执行的分类还有提前编译 (AOT)这一种。
JIT 编译器和传统编译器的区别
- 两者都会生成中间代码(.class)
- 传统编译器会将所有的.class转成机器语言后才能执行程序
- JIT编译器则是在程序运行的过程中将对应的.class文件转成机器语言执行,而且JIT编译器有一些优化技术例如
- 方法内联 - 将一个方法的内容直接嵌入到调用它的地方,以减少方法调用的开销。
- 循环优化 - 改进循环的执行效率,比如通过循环展开减少循环次数。
- 死代码消除 - 移除不会执行到的代码。
- 逃逸分析 - 分析对象的作用域,决定是否可以堆优化,比如栈上内存分配、标量替换、同步锁消除。
- 公共子表达式消除 - 查找并删除代码中重复计算的子表达式。
# 为啥我们还需要解释器呢?
有些开发人员会感觉到诧异,既然 HotSpot VM 中已经内置 JIT 编译器了,那么为什么还需要再使用解释器来 “拖累” 程序的执行性能呢?比如 JRockit VM 内部就不包含解释器,字节码全部都依靠即时编译器编译后执行。
JRockit 虚拟机是砍掉了解释器,也就是只采及时编译器。那是因为呢 JRockit 只部署在服务器上,一般已经有时间让他进行指令编译的过程了,对于响应来说要求不高,等及时编译器的编译完成后,就会提供更好的性能。
注意
首先明确两点:
- 解释器:当程序启动后,解释器可以马上发挥作用,意味着它可以立即开始执行程序代码,不需要事先将代码编译成机器语言。就像是读书时,边读边理解,不需要事先全部读完再去理解。这样做的好处是响应速度快,可以省去编译的时间,使得程序可以立即执行。
- 编译器:编译器在程序执行前需要把代码编译成本地代码(即机器语言),这个过程需要一定的时间。就像是先把一本书读完,理解好,然后再去讲述给别人听。但是,一旦代码被编译为本地代码,它的执行效率通常会比解释执行的代码高。这是因为编译器可以在编译过程中进行优化,而且运行时不需要进行额外的转换。
解释器的主要工作是逐行读取源代码,然后将其转换成机器代码或者中间代码并立即执行。它不会生成独立的可执行文件。
编译器的主要工作是将整个源代码一次性转换成机器代码,并生成一个独立的可执行文件。这个过程称为编译。
尽管 JRockit VM 中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。
在此模式下,在 Java 虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
同时,解释执行在编译器进行激进优化不成立的时候,作为编译器的 “逃生门”(后备方案)。
# Hotspot JVM 为何解释器与 JIT 编译器并存?
快速启动:当 Java 程序启动时,HotSpot VM 首先使用解释器执行字节码,这可以使程序快速启动并运行。解释器逐条解释执行字节码,无需等待整个程序编译完成,这对于提高启动速度非常有利。
运行时性能优化:虽然解释执行可以快速启动程序,但执行速度通常不如编译后的本地代码。因此,HotSpot VM 采用 JIT 编译器在运行时将热点代码(即经常执行的代码)编译成优化的本地机器代码。这种即时编译可以显著提高程序的执行性能。
适应性:HotSpot VM 通过监控程序的运行情况,动态地决定哪些代码应该被 JIT 编译。这种适应性优化确保了在不同的运行时环境和不同的程序行为下,都能获得良好的性能。
平衡资源使用:编译过程是资源密集型的,需要消耗 CPU 和内存资源。通过仅对热点代码进行编译,HotSpot VM 能够在提高性能的同时,避免不必要的资源消耗。
# Hotspot JVM 的执行方式
当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。
注意
解释执行与编译执行在线上环境微妙的辩证关系。机器在热机状态(已经运行了一段时间叫热机状态)可以承受的负载要大于冷机状态(刚启动的时候叫冷机状态)。如果以热机状态时的流量进行切流,可能使处于冷机状态的服务器因无法承载流量而假死。
在生产环境发布过程中,以分批的方式进行发布,根据机器数量划分成多个批次,每个批次的机器数至多占到整个集群的 1/8。曾经有这样的故障案例:某程序员在发布平台进行分批发布,在输入发布总批数时,误填写成分为两批发布。如果是热机状态,在正常情况下一半的机器可以勉强承载流量,但由于刚启动的 JVM 均是解释执行,还没有进行热点代码统计和 JIT 动态编译,导致机器启动之后,当前 1/2 发布成功的服务器马上全部宕机,此故障说明了 JIT 的存在。— 阿里团队
# 案例
// 使用 jvisualvm 或者 jconsole 可以看到 JIT 编译的次数和时间
public class JITTest {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
list.add("让天下没有难学的技术");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 典型的编译器类型
前端编译器:Sun 的 javac、Eclipse JDT 中的增量式编译器(ECJ)。
JIT 编译器:HotSpot VM 的 C1、C2 编译器。
AOT 编译器:GNU Compiler for the Java(GCJ)、Excelsior JET。
# 热点代码及探测方式
是否需要启动 JIT 编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。
关于那些需要被编译为本地代码的字节码,也被称之为 **“热点代码”,JIT 编译器在运行时会针对那些频繁被调用的 “热点代码” 做出深度优化 **,将其直接编译为对应平台的本地机器指令,以此提升 Java 程序的执行性能。
- 一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为 “热点代码”,因此都可以通过 JIT 编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR(On Stack Replacement)编译。
- 一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JIT 编译器才会将这些 “热点代码” 编译为本地机器指令执行。这里主要依靠热点探测功能。
目前 HotSpot VM 所采用的热点探测方式是基于计数器的热点探测。
采用基于计数器的热点探测,HotSpot VM 将会为每一个方法都建立 2 个不同类型的计数器,分别为方法调用计数器(用于统计方法调用次数)和回边计数器(统计 for 或者 while 的运行次数的计数器)。
# 方法调用计数器
# 热度衰减
- 如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)(半衰周期是化学中的概念,比如出土的文物通过查看 C60 来获得文物的年龄)。
- 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样的话,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。
- 另外,可以使用 - XX:CounterHalfLifeTime 参数设置半衰周期的时间,单位是秒。
# 回边计数器
# HotSpot VM 可以设置程序执行方法
缺省情况下 HotSpot VM 是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为 Java 虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。
- -Xint:完全采用解释器模式执行程序。
- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。
- -Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序。
# 速度对比
/**
* 测试解释器模式和 JIT 编译模式
* -Xint : 7387ms
* -Xcomp : 1140ms
* -Xmixed : 1054ms
*/
public class IntCompTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
testPrimeNumber(1000000);
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
}
public static void testPrimeNumber(int count) {
for (int i = 0; i < count; i++) {
//计算 100 以内的质数
label:
for (int j = 2; j <= 100; j++) {
for (int k = 2; k <= Math.sqrt(j); k++) {
if (j % k == 0) {
continue label;
}
}
//System.out.println(j);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# HotSpotVM JIT 分类
在 HotSpot VM 中内嵌有两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,但大多数情况下我们简称为 C1 编译器 和 C2 编译器。开发人员可以通过如下命令显式指定 Java 虚拟机在运行时到底使用哪一种即时编译器,如下所示:
-client:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器。
- C1 编译器会对字节码进行简单和可靠的优化,耗时短,以达到更快的编译速度。
-server:指定 Java 虚拟机运行在 server 模式下,并使用 C2 编译器。
- C2 进行耗时较长的优化,以及激进优化,但优化的代码执行效率更高。(使用 C++)。
# C1 和 C2 编译器不同的优化策略
- 在不同的编译器上有不同的优化策略,C1 编译器上主要有方法内联,去虚拟化、元余消除。
- 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程。
- 去虚拟化:对唯一的实现樊进行内联。
- 冗余消除:在运行期间把一些不会执行的代码折叠掉。
- C2 的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在 C2 上有如下几种优化:
- 标量替换:用标量值代替聚合对象的属性值。
- 栈上分配:对于未逃逸的对象分配对象在栈而不是堆。
- 同步消除:清除同步操作,通常指 synchronized。
注意
逃逸分析主要是与 C2 编译器(Server 模式)关联的优化技术。虽然 C1 编译器的优化重点不在于执行逃逸分析,但这并不意味着在 Client 模式下就完全无法使用逃逸分析或其他形式的优化。C1 编译器依然会执行一些基本的优化以提高程序的启动速度和运行效率,但相比于 C2 编译器,它可能不会执行那么深入的逃逸分析。
# 分层编译策略
- 分层编译(Tiered Compilation)策略:程序解释执行(不开启性能监控)可以触发 C1 编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2 编译会根据性能监控信息进行激进优化。
- 不过在 Java7 版本之后,一旦开发人员在程序中显式指定命令 “-server” 时,默认将会开启分层编译策略,由 C1 编译器和 C2 编译器相互协作共同来执行编译任务。
- 一般来讲,JIT 编译出来的机器码性能比解释器解释执行的性能高。
- C2 编译器启动时长比 C1 慢,系统稳定执行以后,C2 编译器执行速度远快于 C1 编译器。
# Graal 编译器
- 自 JDK10 起,HotSpot 又加入了一个全新的即时编译器:Graal 编译器。
- 编译效果短短几年时间就追平了 C2 编译器,未来可期(对应还出现了 Graal 虚拟机,是用来替代 Hotspot 的虚拟机的)。
- 目前,带着实验状态标签,需要使用开关参数去激活才能使用
-XX:+UnlockExperimentalvMOptions -XX:+UseJVMCICompiler
。
# AOT 编译器
jdk9 引入了 AoT 编译器(静态提前编译器,Ahead of Time Compiler)。
Java 9 引入了实验性 AOT 编译工具 jaotc。它借助了 Graal 编译器,将所输入的 Java 类文件转换为机器码,并存放至生成的动态共享库之中。
所谓 AOT 编译,是与即时编译相对立的一个概念。我们知道,即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。而 AOT 编译指的则是,在程序运行之前,便将字节码转换为机器码的过程。
.java -> .class -> (使用 jaotc) -> .so
AOT 编译器的优点:
性能:由于代码在运行前就已经被编译,所以可以避免运行时编译带来的性能开销。
启动时间:AOT 编译可以减少程序的启动时间,因为不需要在运行时进行编译。
预测性:AOT 编译的性能更加可预测,因为所有的优化都在编译时完成,运行时的性能不会受到 JIT 编译的影响。
AOT 编译器的缺点:
优化程度:由于 AOT 编译在运行前完成,所以无法利用运行时的动态信息进行优化,这可能会导致优化程度不如 JIT 编译。
二进制大小:AOT 编译会将所有的代码都编译成机器语言,这可能会导致生成的二进制文件比 JIT 编译生成的文件大。
跨平台:AOT 编译生成的是特定于某一平台的机器代码,破坏了 java “一次编译,到处运行” 的能力。