JVM编译器与解释器

前言

  1. 建议:学习的时候笔记尽可能充分,尽可能把问题搞懂,最后复习的时候,直接选重要的记忆就ok了(也建议在学习的时候就总结下重点,然后复习的时候过一遍就好了)。
  2. 参考:
    1. 《深入理解Java虚拟机(第3版)》第11章 后端编译与优化
    2. CSDN/必应

疑问

  1. 栈上替换的具体过程?

前置知识

本地代码/本地机器码

  1. 机器码(machine code):学名机器语言指令,有时也被称为原生码(Native Code;本地机器码,本地代码这些都是指机器码),是电脑CPU可以直接解读的数据(只包含0和1)。
    1. 补充1:包含本地xx,指本机CPU可以直接指令的代码。
  2. 无论什么语言写的代码,其到最后都是通过机器码运行的,无一例外。
    1. JVM中编译器(即时编译器JIT、提前编译器AOT)将字节码编译为本地代码(既机器码)

热点代码

  1. 热点代码,主要有两类:
    1. 被多次调用的方法
    2. 被多次执行的循环体
  2. 热点代码是即时编译器编译的对象,上面两种情况,编译的目标对象都是整个方法体,而不会是单独的循环体。
    第2种情况是方法被调用次数较少,但是方法体内部存在循环次数较多的循环体。
    1. 第一种情况,由于是依靠方法调用触发的编译,由于是依靠方法调用触发的编译,那编译器理所当然地会以整个方法作为编译对象,这种编译也是虚拟机中标准的即时编译方式
    2. 第二种情况,尽管编译动作是由循环体所触发的,热点只是方法的一部分,但编译器依然必须以整个方法作为编译对象,只是执行入口(从方法第几条字节码指令开始执行)
      会稍有不同,编译时会传入执行入口点字节码序号(Byte Code Index,BCI)。这种编译方式因为编译发生在方法执行的过程中,因此很形象地称为”栈上替换“(On Stack Replacement,OSR),即方法的栈帧还在栈上,方法就被替换了。
  3. 栈上替换OSR
    1. (其实不需要完整了解其过程,只需要知道是即时编译器编译热点代码的第2种方式就行了)。
    2. (具体热点检测的过程见”热点检测“)。

热点检测

  1. 要知道某段代码是不是热点代码,是不是要触发即时编译,这个行为称为”热点探测“(HotSpot Code Detection),目前主流的热点探测判定方式有两种,分别是:
    1. 基于采样的热点探测:虚拟机周期性检查各个线程的调用栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是”热点方法“。
      1. 优点:简单高效。
      2. 缺点:很难精确地确认一个方法的热度。
    2. 基于计数器的热点探测:为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。
      1. 缺点:实现起来要麻烦一些,需要为每个方法建立并维护计数器。
      2. 优点:统计结果相对来说更加精确严谨。
  2. 两种热点检测方法在商用Java虚拟机中都有使用到,而在HotSpot虚拟机中使用的是第二种基于计数器的热点探测方法
    1. 实现热点计数:为了实现热点计数,HotSpot为每个方法准备了两类计数器——方法调用计数器回边计数器
      (回边:在循环边界往回跳转)。
  3. 方法调用计数器:当一个方法被调用时,虚拟机会先检查该方法是否存在被即时编译过的版本,如果存在,则优先使用编译后的本地代码来执行。
    如果不存在已被编译过的版本,则将该方法的调用计数器增加1,然后判断方法调用计数器与回边计数器之和是否超过方法调用计数器的阈值。
    一旦已超过阈值的话,将会向即时编译器提交一个该方法的代码编译请求(标准编译请求)。
    1. 在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。
      当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那该方法的调用计数器就会减少一半,这个过程被称为方法调用计数器热度的衰减(Counter Decay),
      而这段时间就称为此方法统计的半衰周期(Counter Half Life Time),进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以手动关闭。
  4. 回边计数器:它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令就称为“回边”(Back Edge),很显然建立回边计数器统计的目的是为了触发栈上的替换编译
    1. 当解释器遇到一条回边指令时,会先查找将要执行的代码片段是否有已经编译好的版本,如果有的话,他将会优先执行已编译的代码,
      否则就把回边计数器加1,然后判断方法调用计数器与回边计数器之和是否超过回边计数器的阈值。
      当超过阈值的时候,将会提交一个栈上替换编译请求,并且把回边计数器的值稍微降低一些,以便继续在解释器中执行循环,等待编译器输出编译结果。
  5. 当解释器遇到方法调用指令的时候会判断方法是否已被编译,然后判断xxx是否超过方法调用计数器;当解释器遇到一条回边指令时会查找将要执行的代码片段是否已有编译好的版本,然后判断xxx是否超过回边计数器。
    1. xxx:方法调用计数器和回边计数器之和。

正文

解释器和编译器

  1. 定义:
    1. 解释器:解释器是一种计算机程序,它将每个高级程序语句转换成机器代码。
    2. 编译器:把高级语言编写的程序转换成机器代码。
      1. 注意编译器的定义并没有说明是在运行时还是在运行前翻译(将高级语言转换成机器码)。解释器只可以在运行时翻译,编译器既可以在运行时也可以在运行前,比如即时编译器在运行时翻译,而提前编译器在运行前翻译。一般来说,我们说的编译器是在运行前翻译(很多语言是先编译再执行/运行的)。(因为被这个问题困扰过,所以当下划一下重点,后面可以取消)
  2. 解释器与编译器的比较(偏向于特点比较):
    1. 两者都是将高级语言转换成机器码,解释器在程序运行时将代码转换成机器码,编译器在程序运行之前(一般是这样,但是即时编译器是运行时)将代码转换成机器码。
    2. 解释器一行一行翻译,不产生任何中间代码,不需要太多内存;编译器提前翻译所有内容;生成中间目标代码,需要额外内存。
    3. 解释器读取一条语句显示错误就不能执行下一条语句;编译器在编译时显示所有错误和警告,不修正错误就不能编译成功。
  3. 尽管并不是所有的Java虚拟机都采用解释器与编译器并存的运行架构,但目前主流的商用Java虚拟机,譬如HotSpot、OpenJ9等,内部都同时包含解释器与编译器
  4. 解释器与编译器两者各有优势(偏向于性能比较):
    1. 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。
    2. 当程序启动后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,这样可以减少解释器的中间损耗,获得更高的执行效率
    3. 当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提升效率
  5. 在JVM中,无论是解释器解释执行,还是即时编译器编译成本地代码后执行本地代码,最后都是转换成了本地代码(适合当前计算机运行的指令级/本机CPU可以直接运行的代码,01码),交给CPU执行。
    解释器可以立马启动和执行,省去编译的时间,立即执行,但是解释器每次执行都要解释转换为本地代码很耗时。
    即时编译的时间算作程序运行的时间(运行时编译),所以第一次编译时很耗时,程序启动慢,但是编译后可以存储在本地,以后直接调用该本地代码,执行速度快。
  6. 总结(备战秋招)
    1. 解释器和编译器比较
      1. 定义:解释器将每个高级程序 语句 转换成机器代码;编译器将高级语言编写的 程序 转换成机器代码。
      2. 过程:都是将高级语言转换成机器码的过程。解释器一行一行翻译,边翻译边执行;编译器提前翻译所有内容,然后再执行。
        1. 特别注意:一般编译器都是运行前翻译,比如提前编译器;少数编译器再运行时翻译,比如即时编译器。
      3. 内存资源限制:解释器不会产生中间代码没有额外内存消耗;编译器会保存中间代码有额外消耗。
      4. 使用场景:解释器适合需要迅速启动和执行的时候;编译器适合内存资源限制较小,且不需要迅速启动和执行的时候,时间越长,它的优势越大(相比于解释器)。
    2. 相同点
      1. 都是将高级语言转换成机器码。
      2. 解释器和即时编译器都是运行时工作的。(提前编译器运行前工作)

即时编译器JIT

  1. 引入:
    1. 《深入理解Java虚拟机(第3版)》11.2 即时编译器:“本节我们将会了解HotSpot虚拟机内的即时编译器的运作过程,此外,我们还将解决以下几个问题
      1. 为何HotSpot虚拟机要使用解释器与即时编译器并存的架构?
      2. 为何HotSpot虚拟机要实现两个(或三个)不同的即时编译器?
      3. 程序何时使用解释器执行?何时使用编译器执行?
      4. 哪些程序代码会被编译为本地代码?如何编译本地代码?
      5. 如何从外部观察到即时编译器的编译过程和编译结果?
    2. 尽管并不是所有的Java虚拟机都采用解释器与编译器并存的运行架构,但目前主流的商用Java虚拟机,譬如HotSpot、OpenJ9等,内部都同时包含解释器与编译器,解释器与编译器两者各有优势。
  2. 即时编译器:目前主流的Java虚拟机里,Java程序最初都是通过解释器(Interpreter)进行解释执行的,为了提高“热点代码“的执行效率,在运行时,虚拟机会把“热点代码”编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器。
  3. HotSpot虚拟机中内置了两个(或三个)即时编译器,其中两个编译器存在已久,分别被称为“客户端编译器”和“服务器编译器”,或者简称为C1编译器和C2编译器。
    第三个是JDK10才出现的,长期目标是替代C2的Graal编译器,目前还处于实验状态,不考虑。
  4. 分层编译:由于即时编译器编译本地代码需要占用程序运行时间,通常要编译出优化程度越高的代码,所花费的时间便会越长;而且想要编译出优化程度更高的代码,解释器可能还要体编译器收集性能监控信息,这对解释执行阶段的速度也有所影响。
    为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机在编译子系统中加入了分层编译的功能。
    1. 分层:略
    2. 实施分层编译后,解释器、客户端编译器和服务端编译器就会同时工作,热点代码都可能会被多次编译,用客户端编译器获取更高的编译速度,用服务端编译器来获取更好的编译质量

编译过程(热点代码即时编译)

  1. 在默认条件下,无论是方法调用产生的标准编译请求,还是栈上替换编译请求,虚拟机在编译器还未完成编译之前,都仍然将按照解释方式继续执行代码,而编译动作则在后台的编译线程中进行。
    用户可以手动禁止后台编译,禁止后台编译的话,达到即时编译的条件时,执行线程向虚拟机提交编译请求以后将会一直阻塞等待,直到编译过程完成再开始执行编译器输出的本地代码。
  2. 其他暂略。

提前编译器AOT

  1. 提前编译AOT:在运行前将字节码编译成本地代码。
    1. 是相对于即时编译JIT的概念。最大的好处是无需等待JIT在运行时编译(会占用程序运行时间和运算资源),在运行前将字节码编译成本地代码,运行时可以直接调用;坏处是不满足Java的口号——“一次编译,到处运行”。
  2. 目前主要的实现方式有两种:
    1. 一种是与传统C、C++编译器类似,在程序运行之前把程序代码编译成机器码的静态翻译工作。
      1. 这一种实现方式在Java中的存在价值直指即时编译的最大弱点:即时编译要占用程序运行时间和运算资源。
    2. 另一种是把原本即时编译器在运行时要做的编译工作提前做好并保存下来,下次运行到这些代码(譬如公共库代码在被同一台机器其他Java进程使用)时直接把它加载进来使用。
      1. 这一种方式,本质上是给即时编译器做缓存加速,去改善Java程序的启动时间,以及需要一段时间预热之后才能达到最高性能的问题。这种提前编译被称为动态提前编译或者直接叫即时编译缓存
      2. 与第一种的差别:第一种是静态翻译,第二种和即时编译一样是动态翻译(具体怎么实现的可以先不考虑,动态翻译会有更多优化?)。
  3. 但是即时编译相比于提前编译也有很多优点:
    1. 性能分析制导优化:
      1. 解释器和客户端编译器在运行期间会不断收集性能监控信息,这些信息一般无法在静态分析时获得,或者不一定存在唯一的解,但在动态运行时很容易得到。(第二种提前编译是怎么实现的?)
    2. 激进预测性优化:暂略。
    3. 链接时优化:由于Java天生是动态链接的,所以提前编译无法做到链接后的优化。
  4. (补充)提前编译的一些特点:
    1. 提前编译和“一次编译,到处运行”是冲突的,因为提前编译是平台相关的。
    2. 提前编译的本地二进制码的体积会明显大于字节码的体积。
    3. 提前编译通常要求程序是封闭的,不能在外部动态加载新的字节码。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值