一、概述

即时编译器(Just In Time Compiler),也称为 JIT 编译器,它的主要工作是把热点代码编译成与本地平台相关的机器码,并进行各种层次的优化,从而提高代码执行的效率。

那么什么是热点代码呢?我们知道虚拟机通过解释器(Interpreter)来执行字节码文件,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code)。

即时编译器编译性能的好坏、代码优化程度的高低是衡量一款商用虚拟机优秀与否的关键指标之一,它也是虚拟机最核心且最能体现技术水平的部分。

然而,程序员在开发过程中,压根不会感知到即时编译器的存在,也参与不了即时编译器的过程,所以我们对即时编译器的学习更多的是了解,明白怎么写代码才能更好的被即时编译器优化。

二、工作流程

HotSpot 虚拟机包含解释器和编译器。它们是怎么搭配工作的呢?当程序启动的时候,解释器首先发挥作用,它能直接运行字节码文件;随着时间的推移,越来越多的热点代码被编译器编译成机器码,从而获取更高的执行效率。同时,解释器还可以作为编译器激进优化时的一个“逃生门”,当编译器的激进优化手段不成立时,如加载了新类后类型继承结构出现变化等,可以通过逆优化(Deoptimization)退回到解释状态继续由解释器执行。

编译器又分为两种,C1 编译器(Client Compiler)和 C2 编译器(Server Compiler),HotSpot 虚拟机会选择哪个编译器是由虚拟机运行于 Client 模式还是 Server 模式决定的。

默认情况下,虚拟机采用解释器和一种编译器搭配的方式工作,但是在分层编译策略下,C1 编译器和 C2 编译器将会同时工作,分层编译根据编译器编译、优化的规模和耗时,划分出不同的编译层次:

  • 第0层:程序解释执行,解释器不开启性能监控功能,触发 C1 编译。
  • 第1层:C1 编译,将字节码编译成本地代码,进行简单、可靠的优化,如有必要解释器将开始性能监控。
  • 第2层:C2 编译,将字节码编译成本地代码,启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

tips:

  1. 使用 “-client” 强制虚拟机运行于 Client 模式。
  2. 使用 “-server” 强制虚拟机运行于 Server 模式。
  3. 使用 “-Xint” 强制虚拟机只使用解释器执行程序,编译器不工作。
  4. 使用 “-Xcomp” 强制虚拟机只使用编译器执行程序,解释器作为编译器的“逃生门”。
  5. 使用 “-XX:+TieredCompilation” 开启分层编译。虚拟机 Server 模式下默认开启。

三、热点代码探测

热点代码分为两种:被多次调用的方法、被多次执行的循环体。多次是一个很泛的概念,那么到底什么时候才能把热点代码编译成机器码呢?HotSpot 虚拟机采用的是计数器的方式,它为每个方法(甚至是代码块)建立计数器,统计执行次数,如果执行次数达到一定的阈值,就把这部分代码编译成机器码。

探测“被多次调用的方法”的计数器称为方法调用计数器(Invocation Counter),它统计的是一个方法调用的相对次数,即同一段时间内方法被调用的次数,当超过一定的时间限度,如果该方法的计数仍然不足以让它提交给编译器编译,那么该方法的计数就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),这段时间就被称为此方法统计的半衰周期(Counter Half Life Time)。方法调用计数器的相关 JVM 参数如下:

  1. -XX:CompileThreshold 设置方法调用计数器的阈值,Client 模式下默认是 1500 次, Server 模式下默认是 10000 次
  2. -XX:UseCounterDecay 设置 true/false 来开启/关闭热度衰减,默认开启
  3. -XX:CounterHalfLifeTime 设置半衰期的周期,单位是秒(debug 虚拟机支持)

探测“被多次执行的循环体”的计数器称为回边计数器(Back Edge Counter),它统计的是该方法循环执行的绝对次数,没有计数热度衰减的过程。回边计数器的相关 JVM 参数如下:

  1. -XX:OnStackReplacePercentage OSR比率,Client 模式下默认是 933,Server 模式下默认是 140;
  2. -XX:InterpreterProfilePercentage 解释器监控比率,默认值是 33
  3. Client 模式的回边计数器阈值 = CompileThreshold * OnStackReplacePercentage/100,默认是 13995 次
  4. Server 模式的回边计数器阈值 = CompileThreshold * (OnStackReplacePercentage - InterpreterProfilePercentage)/100,默认是 10700 次

四、优化技术

HotSpot 的优化技术非常全面,实现起来也比较复杂,但是对于理解它们来说却显得没那么困难,我们将列举几项最有代表性的优化技术。

1. 方法内联

方法内联的重要性要优于其他优化措施,它的主要目的有两个,一是去除方法调用的成本,二是为其他优化建立良好的基础。

方法内联的行为很简单,就是把目标方法的代码“复制”到发起调用的方法之中,避免发生真实的方法调用而已。

2. 公共子表达式消除

如果一个表达式 E 已经计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生变化,那么 E 的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果代替 E 就可以了。我们来举个例子来模拟下它的优化过程:

    public static void main(String[] args) {
int a = 1;
int b = 1;
int c = 1;
int d = (c * b) * 12 + a + (a + b * c);
// 1. 提取公共子表达式
int E = c * b;
d = E * 12 + a + (a + E);
// 2. 代数化简
d = E * 13 + a * 2;
}

3. 数组边界检查消除

当我们尝试对数组越界访问的时候,Java 会向我们抛一个 java.lang.ArrayIndexOutOfBoundsException,这对软件开发者来说是一件很好的事情,即使没有专门编写防御代码,也可以避免大部分的溢出攻击,但是对虚拟机来说,意味着每一次的数组访问都带有一次隐含的条件判定操作,即数组边界检查,那么有没有办法消除这种检查呢?

虚拟机一般是在即时编译期间通过数据流分析来确定是否可以消除这种检查,比如 foo[3] 的访问,只有在编译的时候确定 3 不会超过 foo.length - 1 的值,就可以判断该次数组访问没有越界,就可以把数组边界检查消除。

4. 逃逸分析

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

如果能证明一个对象不会逃逸到方法或者线程之外,则可以为这个变量进行一些高效的优化:

1) 栈上分配

如果确定一个对象不会逃逸出方法之外,假如能使用栈上分配这个对象,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集系统的压力将会小很多。然而遗憾的是,目前的 HotSpot 虚拟机还没有实现这项优化。

2)同步消除

如果确定一个对象不会被其他线程访问到,那么这个变量就不存在线程间的争抢,对这个变量实施的同步措施也可以消除掉。

3)标量替换

标量:无法被进一步分解的数据,比如原始数据类型(int、long以及 reference 类型等)

聚合量:可以被持续分解的数据,典型的就是 Java 中对象,它们还可以被分解成成员变量等。

标量替换指的是如果把一个 Java 对象拆散分解,根据程序访问的情况,将其使用到的成员变量恢复到原始类型来访问。

如果能确定一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候就可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。

tips:

  1. -XX:+DoEscapeAnalysis 手动开启/关闭逃逸分析,默认开启,C2 编译器有效
  2. -XX:+PrintEscapeAnalysis 查看逃逸分析的结果(debug 虚拟机支持)
  3. -XX:+EliminateAllocations 手动开启/关闭标量替换,默认开启
  4. -XX:+PrintEliminateAllocations 查看标量替换情况(debug 虚拟机支持)
  5. -XX:+EliminateLocks 手动开启/关闭同步消除,默认开启

五、Graal 编译器展望

Graal 编译器是 JDK10 发布的,由于这个编译器使用 Java 编写,代码清晰,又继承了许多来自 HotSpot 的服务端编译器的高质量优化技术,所以无论是科技企业还是高校研究院,都愿意在它上面研究和开发新技术。从 JDK10 起,Graal 编译器可以替换服务端编译器,成为 HotSpot 分层编译中最顶层的即时编译器。

对于 Graal 编译器的推崇离不开 JDK9 发布的 Java 虚拟机编译器接口(JVM Compiler Interface,JVMCI),JVMCI 使得 Graal 可以从 HotSpot 的代码中分离出来,使得 Graal 编译器不至于像 C2 那样跟 HotSpot 强耦合导致越来越臃肿。

JVMCI 的出现,让我们可以把一个在 HotSpot 虚拟机外部的、用 Java 语言实现的即时编译器(例如 Graal)集成到 HotSpot 中。此外,又可以绕开 HotSpot 的即时编译系统,让编译器直接为应用的类库编译出二进制机器码,将编译器当作一个提前编译器去使用(如 Jaotc)。

Graal 编译器仍处于实验室阶段,尚未商用,但未来有望代替或成为 HotSpot 的下一代技术基础。

JVM系列七(JIT 即时编译器).的更多相关文章

  1. jvm系列(七):jvm调优-工具篇

    16年的时候花了一些时间整理了一些关于jvm的介绍文章,到现在回顾起来还是一些还没有补充全面,其中就包括如何利用工具来监控调优前后的性能变化.工具做为图形化界面来展示更能直观的发现问题,另一方面一些耗 ...

  2. jvm系列(七):如何优化Java GC「译」

    本文由CrowHawk翻译,地址:如何优化Java GC「译」,是Java GC调优的经典佳作. Sangmin Lee发表在Cubrid上的”Become a Java GC Expert”系列文章 ...

  3. JVM系列之:JIT中的Virtual Call

    目录 简介 Virtual Call和它的本质 Virtual Call和classic call Virtual Call优化单实现方法的例子 Virtual Call优化多实现方法的例子 总结 简 ...

  4. JVM系列之:JIT中的Virtual Call接口

    目录 简介 最常用的接口List 多个List的调用 不一样的List调用 总结 简介 上一篇文章我们讲解了Virtual Call的定义并举例分析了Virtual Call在父类和子类中的优化. J ...

  5. jvm系列(七):jvm调优

    转自:https://www.cnblogs.com/ityouknow/p/6437037.html 16年的时候花了一些时间整理了一些关于jvm的介绍文章,到现在回顾起来还是一些还没有补充全面,其 ...

  6. jvm系列(八):jvm知识点总览-高级Java工程师面试必备

    在江湖中要练就绝世武功必须内外兼备,精妙的招式和深厚的内功,武功的基础是内功.对于武功低(就像江南七怪)的人,招式更重要,因为他们不能靠内功直接去伤人,只能靠招式,利刃上优势来取胜了,但是练到高手之后 ...

  7. jvm系列(八):jvm知识点总览

    在江湖中要练就绝世武功必须内外兼备,精妙的招式和深厚的内功,武功的基础是内功.对于武功低(就像江南七怪)的人,招式更重要,因为他们不能靠内功直接去伤人,只能靠招式,利刃上优势来取胜了,但是练到高手之后 ...

  8. jvm系列(四):jvm知识点总结

    原文链接:http://www.cnblogs.com/ityouknow/p/6482464.html jvm 总体梳理 jvm体系总体分四大块: 类的加载机制 jvm内存结构 GC算法 垃圾回收 ...

  9. jvm系列四、jvm知识点总结

    原文链接:http://www.cnblogs.com/ityouknow/p/6482464.html jvm 总体梳理 jvm体系总体分四大块: 类的加载机制 jvm内存结构 GC算法 垃圾回收 ...

随机推荐

  1. Element节点输出到System.out

    protected void writeElementToFile(Element valrespEle) { try { TransformerFactory transformerFactory ...

  2. [转]UEditor编辑器两个版本任意文件上传漏洞分析

    0x01 前言 UEditor是由百度WEB前端研发部开发的所见即所得的开源富文本编辑器,具有轻量.可定制.用户体验优秀等特点 ,被广大WEB应用程序所使用:本次爆出的高危漏洞属于.NET版本,其它的 ...

  3. Vue 双向数据绑定v-model

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. 2018-7-31-C#-判断两条直线距离

    title author date CreateTime categories C# 判断两条直线距离 lindexi 2018-07-31 14:38:13 +0800 2018-05-08 10: ...

  5. 【u233】单词化简

    Time Limit: 1 second Memory Limit: 64 MB [问题描述] 最近情报人员得到了一些经过加密的文章,每个单词都很长.破译人员想到先把单词化简一下,方法是把每个单词尽量 ...

  6. JOISC2014 挂饰("01"背包)

    传送门: [1]:洛谷 [2]:BZOJ 参考资料: [1]:追忆:往昔 •题解 上述参考资料的讲解清晰易懂,下面谈谈我的理解: 关键语句: 将此题转化为 "01背包" 类问题,关 ...

  7. linux 处理器特定的寄存器

    如果你需要测量非常短时间间隔, 或者你需要非常高精度, 你可以借助平台依赖的资源, 一个要精度不要移植性的选择. 在现代处理器中, 对于经验性能数字的迫切需求被大部分 CPU 设计中内在的指令定时不 ...

  8. 【74.00%】【codeforces 747A】Display Size

    time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

  9. linux PCI 寻址

    每个 PCI 外设有一个总线号, 一个设备号, 一个功能号标识. PCI 规范允许单个系统占 用多达 256 个总线, 但是因为 256 个总线对许多大系统是不够的, Linux 现在支持 PCI 域 ...

  10. selenium经过WebDriverWait实现ajax测试

    当前位置:我的异常网» Web前端 » selenium经过WebDriverWait实现ajax测试 selenium经过WebDriverWait实现ajax测试 www.MyException. ...