JIT编译器

1. 什么是Just In Time编译器?

Hot Spot 编译

当 JVM 执行代码时,它并不当即开始编译代码。这主要有两个缘由:html

首先,若是这段代码自己在未来只会被执行一次,那么从本质上看,编译就是在浪费精力。由于将代码翻译成 java 字节码相对于编译这段代码并执行代码来讲,要快不少。java

当 然,若是一段代码频繁的调用方法,或是一个循环,也就是这段代码被屡次执行,那么编译就很是值得了。所以,编译器具备的这种权衡能力会首先执行解释后的代 码,而后再去分辨哪些方法会被频繁调用来保证其自己的编译。其实说简单点,就是 JIT 在起做用,咱们知道,对于 Java 代码,刚开始都是被编译器编译成字节码文件,而后字节码文件会被交由 JVM 解释执行,因此能够说 Java 自己是一种半编译半解释执行的语言。Hot Spot VM 采用了 JIT compile 技术,将运行频率很高的字节码直接编译为机器指令执行以提升性能,因此当字节码被 JIT 编译为机器码的时候,要说它是编译执行的也能够。也就是说,运行时,部分代码可能由 JIT 翻译为目标机器指令(以 method 为翻译单位,还会保存起来,第二次执行就不用翻译了)直接执行。性能优化

第二个缘由是最优化,当 JVM 执行某一方法或遍历循环的次数越多,就会更加了解代码结构,那么 JVM 在编译代码的时候就作出相应的优化。服务器

我 们将在后面讲解这些优化策略,这里,先举一个简单的例子:咱们知道 equals() 这个方法存在于每个 Java Object 中(由于是从 Object class 继承而来)并且常常被覆写。当解释器遇到 b = obj1.equals(obj2) 这样一句代码,它则会查询 obj1 的类型从而得知到底运行哪个 equals() 方法。而这个动态查询的过程从某种程度上说是很耗时的。架构

在主流商用JVM(HotSpot、J9)中,Java程序一开始是经过解释器(Interpreter)进行解释执行的。当JVM发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“热点代码(Hot Spot Code)”,而后JVM会把这些代码编译成与本地平台相关的机器码,并进行各类层次的优化,完成这个任务的编译器称为:即时编译器(Just In Time Compiler,JIT)性能

JIT编译器是“动态编译器”的一种,相对的“静态编译器”则是指的好比:C/C++的编译器优化

JIT并非JVM的必须部分,JVM规范并无规定JIT必须存在,更没有限定和指导JIT。可是,JIT性能的好坏、代码优化程度的高低倒是衡量一款JVM是否优秀的最关键指标之一,也是虚拟机中最核心且最能体现虚拟机技术水平的部分。线程


2. 编译器与解释器

首先,不是全部JVM都采用编译器和解释器并存的架构,但主流商用虚拟机,都同时包含这两部分。翻译

2.1 配合过程

  1. 当程序须要迅速启动而后执行的时候,解释器能够首先发挥做用,编译器不运行从而省去编译时间,当即执行程序server

  2. 在程序运行后,随着时间的推移,编译器逐渐发挥做用,把愈来愈多的代码编译成本地代码以后,能够得到更高的执行效率

  3. 当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可使用解释执行来节约内存;反之,则可使用编译执行来提高效率。

  4. 同时,解释器还能够做为编译器(C2才会激进优化)激进优化时的一个“逃生门”,让编译器根据几率选择一些大多数时候都能提高运行速度的优化手段,当激进优化假设不成立。如:加载了新类后,类型继承结构出现变化,出现“罕见陷阱(Uncommon Trap)”时,能够经过逆优化(Deoptimization)退回到解释状态继续执行 
    (部分没有解释器的虚拟机,也会采用不进行激进优化的C1编译器担任“逃生门”的角色)

    这里写图片描述

2.2 解释器 - Interpreter

Interpreter解释执行class文件,好像JavaScript执行引擎同样

特殊的例子:

  • 最先的Sun Classic VM只有Interpreter
  • BEA JRockit VM则只有Compiler,但它主要面向服务端应用,部署在其上的应用不重点关注启动时间

2.3 编译器 - Compiler

只说HotSpot JVM

1. C1和C2:

HotSpot虚拟机内置了两个即时编译器,分别称为Client Compiler和Server Compiler,习惯上将前者称为C1,后者称为C2

2. 使用C1仍是C2?

HotSpot默认采用解释器和其中一个编译器直接配合的方式工做,使用那个编译器取决于虚拟机运行的模式,HotSpot会根据自身版本和宿主机器硬件性能自动选择模式,用户也可使用“-client”或”-server”参数去指定

  1. 混合模式(Mixed Mode) 
    默认的模式,如上面描述的这种方式就是mixed mode

  2. 解释模式(Interpreted Mode) 
    可使用参数“-Xint”,在此模式下所有代码解释执行

  3. 编译模式(Compiled Mode) 
    参数“-Xcomp”,此模式优先采用编译,可是没法编译时也会解释(在最新的HotSpot中此参数被取消)

    能够看到,个人JVM如今是mixed mode 
    这里写图片描述

重要:↓

在JDK1.7(1.7仅包括Server模式)以后,HotSpot就不是默认“采用解释器和其中一个编译器”配合的方式了,而是采用了分层编译,分层编译时C1和C2有可能同时工做


3. 分层编译

3.1 为何要分层编译?

因为编译器compile本地代码须要占用程序时间,要编译出优化程度更高的代码所花费的时间可能更长,且此时解释器还要替编译器收集性能监控信息,这对解释执行的速度也有影响

因此,为了在程序启动响应时间与运行效率之间达到最佳平衡,HotSpot在JDK1.6中出现了分层编译(Tiered Compilation)的概念并在JDK1.7的Server模式JVM中做为默认策略被开启

3.2 编译层 tier(或者叫级别)

分层编译根据编译器编译、优化的规模与耗时,划分了不一样的编译层次(不仅如下3种),包括:

  • 第0层,程序解释执行(没有编译),解释器不开启性能监控功能,可触发第1层编译。

  • 第1层,也称C1编译,将字节码编译为本地代码,进行简单、可靠的优化,若有必要将加入性能监控的逻辑

  • 第2层(或2层以上),也称为C2编译,也是将字节码编译为本地代码,可是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化

实施分层编译后,C1和C2将会同时工做,许多代码会被屡次编译,用C1获取更高的编译速度,用C2来获取更好的编译质量,且在解释执行的时候解释器也无须再承担收集性能监控信息的任务


4. 编译对象与触发条件

1. 谁被编译了?

编译对象就是以前说的“热点代码”,它有两类:

  1. 被屡次调用的方法 
    • 一个方法被屡次调用,理应称为热点代码,这种编译也是虚拟机中标准的JIT编译方式
  2. 被屡次执行的循环体 
    • 编译动做由循环体出发,但编译对象依然会以整个方法为对象
    • 这种编译方式因为编译发生在方法执行过程当中,所以形象的称为:栈上替换(On Stack Replacement- OSR编译,即方法栈帧还在栈上,方法就被替换了)

2. 触发条件

1. 综述

上面的方法和循环体都说“屡次”,那么多少算多?换个说法就是编译的触发条件。

判断一段代码是否是热点代码,是否是须要触发JIT编译,这样的行为称为:热点探测(Hot Spot Detection),有几种主流的探测方式:

  1. 基于计数器的热点探测(Counter Based Hot Spot Detection) 
    虚拟机会为每一个方法(或每一个代码块)创建计数器,统计执行次数,若是超过阀值那么就是热点代码。缺点是维护计数器开销。

  2. 基于采样的热点探测(Sample Based Hot Spot Detection) 
    虚拟机会周期性检查各个线程的栈顶,若是某个方法常常出如今栈顶,那么就是热点代码。缺点是不精确。

  3. 基于踪影的热点探测(Trace Based Hot Spot Detection) 
    Dalvik中的JIT编译器使用这种方式

2. HotSpot

HotSpot使用的是第1种,所以它为每一个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)

  1. 方法计数器

    • 默认阀值,在Client模式下是1500次,Server是10000次,能够经过参数“-XX:CompileThreshold”来设定

    • 当一个方法被调用时会首先检查是否存在被JIT编译过得版本,若是存在则使用此本地代码来执行;若是不存在,则将方法计数器+1,而后判断“方法计数器和回边计数器之和”是否超过阀值,若是是则会向编译器提交一个方法编译请求

    • 默认状况下,执行引擎并不会同步等待上面的编译完成,而是会继续解释执行。当编译完成后,此方法的调用入口地址会被系统自动改写为新的本地代码地址

    • 还有一点,热度是会衰减的,也就是说不是仅仅+,也会-,热度衰减动做是在虚拟机的GC执行时顺便进行的

  2. 回边计数器

    • 回边,顾名思义,只有执行到大括号”}”时才算+1

    • 默认阀值,Client下13995,Server下10700

    • 它的调用逻辑和方法计数器差很少,只不过遇到回边指令时+一、超过阀值时会提交OSR编译请求以及这里没有热度衰减


5. 编译过程

编译过程是在后台线程(daemon)中完成的,能够经过参数“-XX:-BackgroundCompilation”来禁止后台编译,但此时执行线程就会同步等待编译完成才会执行程序

  1. Client Compiler 
    C1编译器是一个简单快速的三段式编译器,主要关注“局部性能优化”,放弃许多耗时较长的全局优化手段 
    过程:class -> 1. 高级中间代码 -> 2. 低级中间代码 -> 3. 机器代码
  2. Server Compiler 
    C2是专门面向服务器应用的编译器,是一个充分优化过的高级编译器,几乎能达到GNU C++编译器使用-O2参数时的优化强度。

使用参数“-XX:+PrintCompilation”会让虚拟机在JIT时把方法名称打印出来,如图: 
这里写图片描述


6. Java和C/C++的编译器对比

这里不是比Java和C/C++谁快这种大坑问题,只是比较编译器(我认为开发效率上Java快,执行效率上C/C++快)

这种对比表明了经典的即时编译器与静态编译期的对比,其实整体来讲Java编译器有优有劣。主要就是动态编译时间压力大能作的优化少,还要作一些动态校验。而静态编译器没法实现一些开发上颇有用的动态特性。

 

 

转载自:https://www.cnblogs.com/insistence/p/5901457.html

相关文章
相关标签/搜索