JVM成神路之全面详解执行引擎子系统、JIT即时编译原理与分派实现

引言

执行引擎子系统是JVM的重要组成部分之一,在JVM系列的开篇曾提到:JVM是一个架构在平台上的平台,虚拟机是一个相似于“物理机”的概念,与物理机一样,都具备代码执行的能力。但虚拟机与物理机最大的不同在于:物理机的执行引擎是直接建立在处理器、高速缓存、平台指令集与操作系统层面上的,物理机的执行引擎可以直接调用各处资源对代码进行直接执行,而虚拟机则是建立在软件层面上的平台,它的执行引擎则是负责解释编译执行自身定义的指令集代码。同时,也正因Java设计出了JVM虚拟机的结构,从而才使得Java可以不受物理平台限制,能够真正实现“一次编译,到处执行”的理念。

对于执行引擎这块的知识,对于理解JVM是有很大帮助的,但JVM相关现有的文章/书籍资料对这块却少有提及或者泛泛而谈,本篇准备对JVM的执行引擎子系统进行全面的阐述。

一、机器码、指令集与汇编语言、高级语言的关系

在准备对JVM的执行引擎进行分析之前,首先得搞明白机器码、指令集、汇编语言以及高级语言之间的关系,只有当搞清楚这几者之间的关系后才能更好的弄懂JVM的执行引擎原理。

1.1、机器码

机器码也被称为机器指令码,也就是指各种由二进制编码方式表示的指令(011101、11110等),最开始的程序员就是通过这种方式编写程序,用这种方式编写出的代码可以直接被CPU读取执行,因为最贴近硬件机器,所以也是执行速度最快的指令。但因为这种指令和CPU之间是紧紧相关的,所以不同种类的CPU对应的机械指令也不同。同时,机械指令都是由二进制数字组成的指令,对于人来说,实在太过繁杂、难以理解且不容易记忆,容易出错,最终指令的方式代替了这种编码方式。

1.2、指令与指令集

由于机器码都是由0和1组成的指令代码,可读性实在太差,所以慢慢地推出了指令,用于替代机器码的编码方式。指令是指将机械码中特定的0和1组成的序列,简化为对应的指令,如INC、DEC、MOV等,从可读性上来说,对比之前的二进制序列组成的机器码要好上许多。但由于不同的硬件平台的组成架构也不同,所以往往在执行一个指令操作时,对应的机器码也不同,所以不同的硬件平台就算是同一个指令(如INC),对应的机器码也不同。

同时,正是因为不同的硬件平台支持的指令是有些稍许不同的,所以每个平台所支持的指令则被称为对应平台的指令集。比如X86架构平台对应的X86指令集、ARM架构平台对应的ARM指令集等。

1.3、汇编语言

前面虽然通过了指令和指令集的方式替代了之前由0和1序列组成的机器码,但指令的可读性相对来说还是比较差的,所以人们又发明了汇编语言。在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)以及标号(Label)代替指令或操作数的地址。在不同的平台,汇编代码对应不同的指令集,但由于计算机只认机器码,所以通过汇编语言编写的程序必须还要经过汇编阶段,变为计算机可识别的机器指令码才可执行。

1.4、高级语言

为了使得开发人员编写程序更为简易一些,后面就涌现了各种高级语言,如Java、Python、Go、Rust等。高级语言对比之前的机器码、指令、汇编等方式,可读性更高,代码编写的难度更低。但通过高级语言编写出的程序,则需要先经过解释或编译过程,先翻译成汇编指令,然后再经过汇编过程,转换为计算机可识别的机器指令码才能执行。

OK~,简单的叙述了一下机器码、指令集与汇编语言、高级语言的关系,从这段阐述中可以得知,Java属于一门高级语言,在执行的时候需要将它编写的代码先编译成汇编指令,再转换为机械指令才能被计算机识别。但似乎我们在使用Java过程中,好像没有这个过程呀?这样因为什么原因呢?

这是因为Java存在JVM这个虚拟平台,JVM的主要任务是负责将javac编译后生成的字节码文件装载到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表和其他辅助信息,这些Java字节码指令是无法直接被OS识别的。那么一个Java程序可以在操作系统上跑起来的根本原因在于什么呢?答案是:依靠于JVM的执行引擎子系统。

二、初窥JVM执行引擎与源码编译原理

Java的执行引擎子系统的主要任务是将字节码指令解释/编译成对应平台上的本地机器指令,简单来说,JVM执行引擎是充当Java虚拟机与操作系统平台之间的“翻译官”的角色。

而目前主要的执行技术有:解释执行、静态编译、即时编译、自适应优化、芯片级直接执行,释义如下:

  • 解释执行:程序在运行过程中,只有当每次用到某处代码时,才会将某处代码转换为机器码交给计算机执行。
  • 静态编译:所谓的静态编译是指程序在启动前,先根据对应的硬件/平台,将所有代码全部编译成对应平台的机器码。
  • 即时编译:程序运行过程中,通过相关技术(如HotSpot中的热点探测)动态的探测出运行比较频繁的代码,然后在运行过程中,将这些执行比较频繁的代码转换机械码并存储下来,下次执行时则直接执行机器码。
  • 自适应优化:开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行仔细优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。
  • 芯片级直接执行:也就是直接编写机器码的方式,编写出的代码可以直接被CPU识别,读取后可以直接执行。

如上便是现有的一些执行技术,在其中解释执行属于第一代JVM,即时编译JIT属于第二代JVM,自适应优化(目前Sun的Hotspot采用这种技术)则吸取第一代JVM和第二代JVM的经验,采用两者结合的方式。而静态编译的技术在BEA公司的JRockit虚拟机以及JDK9的AOT编译器中都实现了,这样做的好处在于:执行性能堪称最佳,但缺点在于:启动的时间会很长,同时也打破了Java“一次编译,到处运行”的原则。

其实在Java刚诞生时,JDK1.0的时候,Java的定位是一门解释型语言,也就是将Java程序编写好之后,先通过javac将源码编译为字节码,再对生成的字节码进行逐行 解释执行 。但这样就导致了程序执行速度比较缓慢,启动速度也并不乐观,因为启动时需对于未编译的.java文件进行编译,而且编译之后生成的字节码指令也不能被计算机识别,还需要在执行时再经过一次 解释 后,才能变为计算机可识别的机器码指令,从而才能使得代码被机器执行。
经过如上分析,JDK1.0时的这种解释执行的缺点非常明显,Java为了做到“一次编译,到处运行”这个准则,将程序的综合性能大大拉低了,为什么呢?因为对比其他语言多了一个步骤。一般来说,一个Java程序想要运行,必须要经过 先编译,再解释 的过程才可以真正地执行。而我们此时再来看看其他语言的执行。

纯编译型语言:在程序启动时,将编写好的源码全部编译为所处平台的机械码指令。
特点:执行性能最佳,启动时间较长,移植性差,不同平台需要重新发包。
纯解释型语言:在程序运行过程中,需要执行某处代码时,再将该代码解释为平台对应的机械码指令,然后交由计算机执行。
特点:启动速度快,执行性能较差,移植性较好。

OK~,简单的看了一下解释型和编译型的语言特点之后,再回过头来想想1.0版本的Java,是不是发现Java因为虚拟机的存在,搞的不上不下的,卡在了中间。因为在Java程序运行时,既要编译源码,又要解释执行,所以最终导致执行性能一般,启动速度也一般。

再到后来,Java为了解决这个问题,在1.2的时候推出了一款后端编译器,也就是JIT即时编译器(后面分析),它可以支持在Java在执行过程中动态生成本地的机械码。现代的高性能JVM都是采用解释器与即使编译器共存的模式工作,所以Java也被称为“半解释半编译型语言”。

而本篇则会基于目前的HotSpot虚拟机对JVM的执行引擎进行分析,它的执行引擎中也采用解释器与即使编译器共存的模型工作,但这款虚拟机的执行模式采用的是 自适应优化 方案执行。

2.1、执行引擎工作过程

对于执行引擎而言,要求所有厂商在实现时,输入输出都必须一致,也就是执行引擎接受的输入内容必须为字节码的二进制流数据,而输出的则必须为程序的执行结果。而执行引擎到底需要执行什么操作,完全是依赖与PC寄存器(程序计数器)的,每当执行引擎处理完一项指令操作后,程序计数器就需要更新下一条需要被执行的指令地址。

在执行Java方法过程中,执行引擎也有可能会根据栈帧中操作数栈的引用信息,直接去访问存储在堆中的Java对象实例数据,也有可能会通过实例对象的对象头中记录的元数据指针(KlassWord)去定位对象的类型信息,也就是会通过元数据指针去访问元数据空间(方法区)中的数据。如下图:

2.1.1、Java源码编译过程

在之前提及过,JVM只识别字节码文件,所以当编写好.java后缀的Java源码时,我们往往还需要通过javac这样的源码编译器(前端编译器),对Java代码进行编译生成.class后才能被JVM装载进内存,源码编译过程如下:

编译是指将一种语言规范转化成另外一种语言规范,通常编译器都是将便于人理解的语言规范(编程语言)转化成机器容易理解的语言规范(由二进制序列组成的机械码)。比如C/C++或汇编语言都是将源代码直接编译成目标机器码。

javac作为Java语言的源码编译器,它编译的目的却不是为了针对于某个硬件平台进行编译的,而是为JVM进行编译,javac的任务就是将Java源代码转换为JVM可识别的字节码,也就是.java文件到.class文件的过程。对于怎么消除不同种类,不同平台之间的差异这个任务就交由JVM来处理,由JVM中的执行引擎来负责将字节码指令翻译成当前程序所在平台可识别的机械码指令。

javac编译过程具体释义如下:

  • ①词法分析:先读取源代码的字节流数据,然后根据源码语言的语法规则找出源代码中的定义的语言关键字,如if、else、while、for等,然后判断这些关键字的定义是否合法,对于合法的关键字生成用于语法分析的记
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值