写点什么

《JVM 系列》 第七章 -- 字节码执行引擎

  • 2022 年 4 月 23 日
  • 本文字数:3957 字

    阅读完需:约 13 分钟

翻译成本地代码后,就可以做一个缓存操作,存储在方法区中


[](()机器码、指令、汇编语言




机器码:


  • 各种用二进制编码方式表示的指令,叫做机器指令码。开始,人们就用它采编写程序,这就是机器语言。

  • 机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用它编程容易出差错。

  • 用它编写的程序一经输入计算机,CPU 直接读取运行,因此和其他语言编的程序相比,执行速度最快。

  • 机器指令与 CPU 紧密相关,所以不同种类的 CPU 所对应的机器指令也就不同。


指令:


  • 由于机器码是有 0 和 1 组成的二进制序列,可读性实在太差,于是人们发明了指令。

  • 指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令(一般为英文简写,如 mov,inc 等),可读性稍好

  • 由于不同的硬件平台,执行同一个操作,对应的机器码可能不同,所以不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同。


指令集:


  • 不同的硬件平台,各自支持的指令,是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集。

  • 如常见的:x86 指令集,对应的是 x86 架构的平台、ARM 指令集,对应的是 ARM 架构的平台


汇编语言:


  • 由于指令的可读性还是太差,于是人们又发明了汇编语言。

  • 在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址。在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。

  • 由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别和执行。


高级语言:


  • 为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言。

  • 高级语言比机器语言、汇编语言更接近人的语言当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序或编译程序。

  • 高级语言也不是直接翻译成 机器指令,而是翻译成汇编语言吗,如下面说的 C 和 C++



C、C++源程序执行过程:


  • 编译过程又可以分成两个阶段:编译和汇编

  • 编译过程: 是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码

  • 汇编过程: 实际上指把汇编语言代码翻译成目标机器指令的过程。



字节码:


  • 字节码是一种中间状态(中间码)的二进制代码(文件),它比机器码更抽象,需要直译器转译后才能成为机器码

  • 字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。

  • 字节码的实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译为可以直接执行的指令。

  • 字节码典型的应用为:Java bytecode


[](()解释器




JVM 设计者们的初衷仅仅只是单纯地为了满足 Java 程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。



解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。当一条字节码指令被解释执行完成后,接着再根据程序计数器中记录的下一条需要被执行的字节码指令执行解释操作。


解释器分类:


在 Java 的发展历史里,一共有两套解释执行器,即古老的字节码解释器、现在普遍使用的模板解释器


  • 字节码解释器在执行时通过纯软件代码模拟字节码的执行,效率非常低下。

  • 模板解释器将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,从而很大程度上提高了解释器的性能。


在 HotSpot VM 中,解释器主要由 Interpreter 模块和 Code 模块构成:


  • Interpreter 模块: 实现了解释器的核心功能

  • Code 模块: 用于管理 HotSpot VM 在运行时生成的本地机器指令


由于解释器在设计和实现上非常简单,因此除了 Java 语言之外,还有许多高级语言同样也是基于解释器执行的,比如 Python、Perl、Ruby 等。但是在今天,基于解释器执行已经沦落为低效的代名词


为了解决这个问题,JVM 平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。


[](()JIT 编译器




Java 代码的执行分类:


  • 第一种是将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行

  • 第二种是编译执行(直接编译成机器码)。现代虚拟机为了提高执行效率,会使用即时编译技术(JIT,Just In Time)将方法编译成机器码后再执行


HotSpot VM 是目前市面上高性能虚拟机的代表作之一。它采用解释器与即时编译器并存的架构。在 Java 虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。


问题来了:


有些开发人员会感觉到诧异,既然 HotSpot VM 中已经内置 JIT 编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如 JRockit VM 内部就不包含解释器,字节码全部都依靠即时编译器编译后执行。


JRockit 虚拟机是砍掉了解释器,也就是只采及时编译器。那是因为呢 JRockit 只部署在服务器上,一般已经有时间让他进行指令编译的过程了,对于响应来说要求不高,等及时编译器的编译完成后,就会提供更好的性能


首先明确:


当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。


编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后,执行效率高。


所以:


尽管 JRockit VM 中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。


在此模式下,当 Java 虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。


同时,解释执行在编译器进行激进优化不成立的时候,作为编译器的“逃生门”


HotSpot JVM 执行方式:


当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。


不同的编译器:


  • Java 语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一些)把.java 文件转变成.class 文件的过程;

  • 也可能是指虚拟机的后端运行期编译器(JIT 编译器,Just In Time Compiler)把字节码转变成机器码的过程。

  • 还可能是指使用静态提前编译器(AOT 编译器,Ahead of Time Compiler)直接把.java 文件编译成本地机器代码的过程。


前端编译器: Sun 的 Javac、Eclipse JDT 中的增量式编译器(ECJ)。


JIT 编译器: HotSpot VM 的 C1、C2 编译器。


AOT 编译器: GNU Compiler for the Java(GCJ)、Excelsior JET。


Graal 编译器:


自 JDK10 起,HotSpot 又加入了一个全新的及时编译器:Graal 编译器,编译效果短短几年时间就追评了 G2 编译器,未来可期。


目前,带着实验状态标签,需要使用开关参数去激活才能使用:-XX:+UnlockExperimentalvMOptions -XX:+UseJVMCICompiler


AOT 编译器:


jdk9 引入了 AOT 编译器(静态提前编译器,Ahead of Time Compiler),所谓 AOT 编译,是与即时编译相对立的一个概念。我们知道,即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。而 AOT 编译指的则是,在程序运行之前,便将字节码转换为机器码的过程。



  • Java 9 引入了实验性 AOT 编译工具 aotc。它借助了 Graal 编译器,将所输入的 Java 类文件转换为机器码,并存放至生成的动态共享库之中。>.java -> .class -> (使用 jaotc) -> .so


  • 好处: Java 虚拟机加载已经预编译成二进制库,可以直接执行。不必等待及时编译器的预热,减少 Java 应用给人带来“第一次运行慢” 的不良体验


  • 缺点: 破坏了 java “ 一次编译,到处运行”,必须为每个不同的硬件,OS 编译对应的发行包。降低了 Java 链接过程的动态性,加载的代码在编译器就必须全部已知。还需要继续优化中,最初只支持 Linux X64 java base


热点探测技术:


  • 一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过 JIT 编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此被称之为栈上替换,或简称为 OSR(On Stack Replacement)编译。

  • 一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JIT 编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。

  • 目前 HotSpot VM 所采用的热点探测方式是基于计数器的热点探测。

  • 采用基于计数器的热点探测,HotSpot V 将会为每一个方法都建立 2 个不同类型的计数器,分别为方法调用计数器(Invocation Counter)用于统计方法的调用次数和回边计数器(Back Edge Counter)用于统计循环体执行的循环次数。


方法调用计数器:


  • 这个计数器就用于统计方法被调用的次数,它的默认阀值在 Client 模式下是 1500 次,在 Server 模式下是 10000 次。超过这个阈值,就会触发 JIT 编译。

  • 这个阀值可以通过虚拟机参数 -XX:CompileThreshold 来设定。

  • 当一个方法被调用时,会先检查该方法是否存在被 JIT 编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阀值。如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。



热点衰减:

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
《JVM系列》 第七章 -- 字节码执行引擎_Java_爱好编程进阶_InfoQ写作社区