浅谈对JIT编译器的理解。

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

Hot Spot 编译

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

首先,如果这段代码本身在将来只会被执行一次,那么从本质上看,编译就是在浪费精力。因为将代码翻译成 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. 当程序需要迅速启动然后执行的时候,解释器可以首先发挥作用,编译器不运行从而省去编译时间,立即执行程序
  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
    • 它的调用逻辑和方法计数器差不多,只不过遇到回边指令时+1、超过阀值时会提交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编译器有优有劣。主要就是动态编译时间压力大能做的优化少,还要做一些动态校验。而静态编译器无法实现一些开发上很有用的动态特性

时间: 2024-12-11 15:49:25

浅谈对JIT编译器的理解。的相关文章

【转载】李航博士的《浅谈我对机器学习的理解》 机器学习与自然语言处理

李航博士的<浅谈我对机器学习的理解> 机器学习与自然语言处理 [日期:2015-01-14] 来源:新浪长微博  作者: 李航 [字体:大 中 小] 算算时间,从开始到现在,做机器学习算法也将近八个月了.虽然还没有达到融会贯通的地步,但至少在熟悉了算法的流程后,我在算法的选择和创造能力上有了不小的提升.实话说,机器学习很难,非常难,要做到完全了解算法的流程.特点.实现方法,并在正确的数据面前选择正确的方法再进行优化得到最优效果,我觉得没有个八年十年的刻苦钻研是不可能的事情.其实整个人工智能范畴

浅谈元类(个人理解)

浅谈元类(个人理解) 我们知道在python中一切皆对象(object),那么所以有的对象都成了object的子类,那么object类又是由谁创建的呢? 实际上,能够创建类的类,叫元类.还有一个概念就是:元类创建了object类,同时又是object类的子类(这是什么鬼?我们就不去考虑这个逻辑了,已经完美解决了先有鸡还是先有蛋的问题了). 元类实际还是所有类的创建者,即使所有类(包括元类自己)都是object的子类,他们的关系如下: Interesting...... 原文地址:https://

10 浅谈 装饰器模式的理解与使用

在不改变现有类结构的情况下,为这个类添加一个新的功能,这就是装饰器模式 所属与结构型模式, 这种模式的特点是:需要创建一个装饰类来包装原有的类,并且提供额外的功能. 为了解决:平时使用继承的方式,会带入许多静态的方法,导致子类过度饱和,膨胀. 举例子:悟空拥有七十二变,不管它怎么变化,他的本质还是一只猴子,但是他又有变化后的一些本领(功能) 实现:装饰类包含原有类的实例对象,即可实现装饰器模式 开始: 定义一个形状的接口 //画图接口 public interface Shape { void

浅谈对差分隐私的理解

在听完第五组的报告之后,浅谈一下对差分隐私的认识,主要针对差分隐私的思想做一个大致的梳理. 为什么会产生差分隐私? 由于有些“聪明”的用户为了知道某些信息,可以通过两次查询结果的差异进行对比,从而在两次数据的对比中找到有用的信息.正如在杨顼组的报告中提到的查询二等兵约瑟夫阿伦是否阵亡的信息,可以通过查询D5和D6两次数据结果,将两次数据结果进行对比就可以知道约瑟夫阿伦是否阵亡的消息. 差分隐私的主要思想: 差分隐私是基于噪音的安全计算方法,它的思想是:对计算过程用噪音干扰,让原始数据淹没在噪音中

浅谈对闭包(Closure)的理解

在理解闭包前理解JavaScript的作用域是有必要的,如果有兴趣,请移步"对JavaScript作用域的认识" 什么是闭包 闭包就是指有权访问另一个函数作用域中变量的函数,通俗点讲闭包就是能够读取其他函数变量的函数.创建闭包的常见方式,就是在一个函数内部创建另一个函数. 1 function fn1() { 2 var a = 1 ; 3 function fn2() { 4 alert(a); 5 } 6 return fn2; 7 } 8 9 var result = fn1()

浅谈我对机器学习的理解

算算时间,从开始到现在,做机器学习算法也将近八个月了.虽然还没有达到融会贯通的地步,但至少在熟悉了算法的流程后,我在算法的选择和创造能力上有了不小的提升.实话说,机器学习很难,非常难,要做到完全了解算法的流程.特点.实现方法,并在正确的数据面前选择正确的方法再进行优化得到最优效果,我觉得没有个八年十年的刻苦钻研是不可能的事情.其实整个人工智能范畴都属于科研难题,包括模式识别.机器学习.搜索.规划等问题,都是可以作为独立科目存在的.我不认为有谁可以把人工智能的各个方面都做到极致,但如果能掌握其中的

浅谈对ionic项目的理解

在思考怎么将客户端app连接到服务器的时候,就在想ionic项目的本质是什么,一开始因为ionic serve这一命令,我以为它自己就是个服务器,但是后来一细想又感觉不是这样,不然客户端又该怎么和服务端进行交互呢?而ionic本质正好验证了我的猜想. 通过ionic info我们可以发现ionic项目所需环境如下: Your system information: ordova CLI: 6.4.0 Ionic CLI Version: 2.1.17 Ionic App Lib Version:

李航博士:浅谈我对机器学习的理解

原文:http://www.itongji.cn/article/06294DH015.html 机器学习方法非常多,也很成熟.下面我挑几个说. 首先是SVM.因为我做的文本处理比较多,所以比较熟悉SVM.SVM也叫支持向量机,其把数据映射到多维空间中以点的形式存在,然后找到能够分 类的最优超平面,最后根据这个平面来分类.SVM能对训练集之外的数据做很好的预测.泛化错误率低.计算开销小.结果易解释,但其对参数调节和核函数的参 数过于敏感.个人感觉SVM是二分类的最好的方法,但也仅限于二分类.如果

浅谈对增长黑客的理解

Growth hacker是2010硅谷提出来的, 线上有很多相关的文章, 国内也有范冰出的一本同名书籍, 知乎上也有不少讨论. 我在这里只说一下, 自己对growth hacking的肤浅理解. 首先是字面上的理解, growth hacking. growth是目标, hacking是手段和方法, 可以翻译成“破解”. 通过hacking手段实现怎长目标的人被叫做growth hacker.  以前能够破解软件, 攻陷网站,拿下root权限的人被叫做黑客hacker.我多年前也做过黑客, 拿