写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记。其观看地址如下:尚硅谷2020最新版宋红康JVM教程

执行引擎是Java虚拟机中的核心组成部分。

执行引擎的作用就是解析虚拟机字节码指令,即执行一条条的代码流程,并得到执行结果。

我们可以先来看一下执行引擎在Java虚拟机中的位置,



可以看出,一个Java线程就是一个执行引擎的实例。则在一个JVM实例中就会有很多个执行引擎在工作,可能有的在执行我们编写的应用程序,有的在执行JVM内部程序,如垃圾收集器等。

1、执行引擎的工作过程

“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行功能。其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约,自由地去定制指令集和执行引擎的体系结构,并执行那些不被硬件直接支持的指令集格式。

指令集:所谓指令集,就是在CPU中用来计算和控制计算机系统的一套指令的集合,每一种新型的CPU在设计时都规定了一系列与其他硬件电路相匹配的指令系统。即指令集是用来操纵计算机硬件资的。指令集跟“物理机”上的硬件是绑定的,不同类型的CPU,它的指令集也是不同的。

JVM想要成功运行,也必须跟物理机一样有一套指令集。JVM的指令集就是我们常说的Java字节码。所以,任何符合class文件规范的Java字节码指令都可以被JVM执行。

但字节码并不能够直接运行在操作系统上,因为字节码不等同于本地机器指令(能被机器直接执行的代码)。字节码仅仅包含有能被JVM所识别的字节码指令、符号表,以及其他的辅助信息。

想要让一个Java程序运行起来,执行引擎的作用就是将字节码指令解释(或者编译)成对应平台上的本地机器码。即JVM中的执行引擎充当了将高级语言编译成机器语言的译者。

如图所示,



在JVM中,执行引擎需要执行什么样的字节码指令,完全依赖于程序计数器,程序计数器保存着当前字节码指令的地址。

每执行完一次指令操作后,程序计数器就会更新下一条需要被执行的指令地址。

当然方法在执行过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型。

从宏观上看,所有的Java虚拟机的执行引擎输入输出都是一致的:输入的都是字节码二进制流,处理过程就是字节码解释执行(或编译执行)的过程,输出的都是执行结果。

2、Java代码编译和执行的过程

2.1、编译过程

我们知道,任何一种高级语言,都需要经过翻译才能被实体机器所识别并执行,这个“翻译”的过程就是我们常说的编译,编译器就是专门用于编译工作的。

通常编译器都是将便于人们理解的语言转化为机器能识别的机器码,如C/C++或者汇编语言都是直接将源代码编译成目标机器能识别的二进制机器码。这个机器码也就是CPU能够直接执行的指令集合。

Java语言常用的编译器是Javac,这是JDK自带的一个编译器。但Javac并不是直接将Java源代码编译成机器码,而是编译成JVM能够识别并执行的另一种语言——Java字节码。由一条条字节码组成的文件就是我们常说的字节码文件(.class文件)。

那么字节码指令又如何“翻译”成CPU识别的机器码呢?

这就是JVM中的执行引擎的任务了。

下面我们先简单介绍Java源代码编译成字节码的过程。

一般的流程如下,



词法分析:

将源代码一字节一字节地读入,然后找出这些字节中的语法关键词(如if、for、while等Java定义的保留关键词),其结果就是从源码中找出符合规范的Token(标记)流。就好比给一句话,我们要找出那些是特殊的词语,那些不是。其结果就是找出源码中所有的Java关键字。

语法分析:

检查词法分析中找出来的关键词组合在一起是否符合Java语言规范,如词法分析中找出来一个关键词if,那么语法分析就要检查这个if后面跟着的是不是一个布尔判断表达式,即使用if这个关键词时要符合Java的语法规范。其结果就是形成一个语法树。

语义分析:

即分析语法树,把一些浮渣的语法转化成简单语法。其结果就是使语法更贴近目标语言的语法规划,形成一个注解过后的语法树(比如把foreach转化成for循环,并加上注解)。

字节码生成:

经过以上几步后,Javac会通过其字节码生成器组件生成字节码文件。

Javac的主要模块就是词法分析器、语法分析器、语义分析器和代码生成器。它们共同完成将Java源码转化成字节码的任务。

2.2、执行过程

Java源码经过编译后,会成为Java字节码。而字节码的执行是由JVM中的执行引擎来完成的。

其执行流程如下,



可以看出,JVM的执行引擎在执行字节码时,有通过解释器逐行解释执行和通过JIT编译器编译产生本地代码再执行这两种方式,

什么是解释器和JIT编译器?

解释器(Interpreter):

当JVM启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。

JIT编译器(Just In Time Complier)

JVM会将字节码直接编译成和本地机器平台相关的机器语言,缓存起来,然后再执行。

在JDK1.0时,JVM采用的是解释执行,后来又发展出了可以直接生成本地代码的即时编译器。

现在JVM在执行Java字节码时,都会将解释执行与编译执行二者结合。

故Java也被称为半编译半解释型语言。

3 、解释器和JIT编译器

3.1、解释器

解释器真正意义上所承担的角色是一个运行时的“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。

当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令进行解释执行

在HotSpot虚拟机中,解释器主要由Interpreter模块和Code模块构成。

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

Code模块:用于管理虚拟机在运行时生成的本地机器指令。



由于解释器在设计和实现上非常简单,因此除了Java语言外,还有许多高级语言也同样基于解释器执行,如Pyhton、Perl、Ruby等。但基于解释器执行的效率总是十分低下。

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

3.2、JIT编译器

现代虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后再执行。

HotSpot虚拟机是目前市面上高性能虚拟机的代表作之一。它采用解释器与即时编译器并存的架构,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。

为了提高热点代码的执行效率,在运行时,虚拟机会把这些代码编译成与本地平台相关的机器码,并运行各层次的优化,完成这个任务的编译器被称为JIT即时编译器。

3.3、为什么HotSpot要一起使用JIT编译器和解释器?

首先,当程序启动后,解释器可以马上发挥作用,省去编译时间立即执行。而编译器想要发挥作用,得先把代码编译成本地代码,这需要一定的执行时间。所以,如果只使用JIT编译器的话,程序在启动时,必然需要花费更长的时间来编译。而两者共同使用,在程序启动时,解释器立即执行,省去了编译时间。当越来越多的字节码被JIT编译器编译成本地代码后,,可以获得更高的执行效率。

其次,当程序运行环境所能利用的内存资源有限时,可以使用解释器执行以节约内存。若内存充足,则可以使用JIT编译器提升效率。

而且,虽然说JIT编译器比解释器快,但实质上说的是执行编译后的目标代码比边解释边执行快。对于一些只执行一次的代码(如类的初始化方法< clinit >),如果算上编译所需要的时间,实际上解释执行比JIT编译执行要快。只有对于频繁执行的热点代码,JIT编译才能确保编译后执行带来的时间优势能抵消掉编译所需的时间开销。因此,使用解释器执行很少使用或只使用一次的代码,而用JIT编译器执行热点代码,可以在一定程度上保证效率。

同时,当执行JIT编译器编译后的代码出现罕见的错误时,可以通过逆优化的手段,退后到解释状态继续执行。

3. 4、热点代码及探测方式

一个被多次调用的方法,或者是一个方法体内部循环次数达到一定次数的代码被称为热点代码。这些代码会被JIT编译器编译为对应平台的本地机器指令。由于JIT编译发生在方法执行过程中,因此也被称为栈上替换(On Stack Replacement)。

HotSpot虚拟机要如何判断出热点代码?

判断一段代码是否为热点代码的行为被称为“热点探测”,常见的热点探测方式有:

  • 基于计数器的探测:JVM会为每个方法(或每个代码块)建立计数器,统计执行次数,如果超过阀值那么就是热点代码。缺点是维护计数器开销。
  • 基于采样的探测:JVM会周期性检查各个线程的虚拟机栈的栈顶,如果某个方法经常出现在栈顶,那么就是热点代码。缺点是不精确。

HotSpot采用的热点探测方式是基于计数器的热点探测。HotSpot虚拟机会为每个方法都建立两个不同类型的计数器,分别为方法调用计数器(Invocation Counter) 和 回边计数器(Back Edge Counter)。

方法计数器用于统计方法的调用次数。回边计数器则用于统计循环体执行的循环次数。

3.5、方法调用计数器

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

可以通过虚拟机参数 -XX:Complier Threshold 来设定阈值。

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



如果不做任何设置,方法调用计数器统计的并不是方法的绝对调用次数,而是一段时间内的方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那么这个方法的调用计数器值就会减半,这个过程称为方法调用计数器的热度衰减(Counter Decay),而这段时间被称为方法的半衰周期( Counter Half Life Time )。

进行热度衰减的动作是在虚拟机进行垃圾回收时一起进行的,可以使用虚拟机参数来关闭热度衰减,让方法计数器统计方法的绝对调用次数。这样,只要系统运行的时间足够长,绝大部分方法都会被编译成本地代码。当然也可以使用虚拟机参数来设置半衰期的时间,单位是秒。

-XX:+/-UseCounterDecay 开启/关闭热度衰减

-XX:CounterHalfLifeTime 设置半衰期时间,单位秒

3.6、回边计数器

回边计数器的作用是统计方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为回边(Back Edge)。

显然,建立回边计数器的目的就是为了触发OSR编译。

3.7、HotSpot设置程序执行方式

在缺省情况下(即未设置参数时)HotSpot虚拟机是采用解释器与即时编译器并存的架构。

开发人员也可以根据具体的应用场景,通过命令显式地为JVM指定在运行时采用的是完全解释执行,或者是完全采用编译执行,亦或是混合执行(默认)。

虚拟机设置的参数如下,

-Xint: 完全采用解释器模式执行程序。

-Xcomp: 完全采用即时编译器模式执行程序(如果编译器出现问题,解释器就会介入)。

-Xmixed: 采用解释器加即时编译器的混合模式共同执行程序。

3.8、C1编译器和C2编译器

在HotSpot虚拟机中内置了两个JIT编译器,分别为Client Compiler和Server Compiler,我们平时简称为C1编译器和C2编译器。

C1编译器会对字节码进行简单和可靠的优化,耗时短,以达到更快的编译速度。C2编译器进行耗时较长的优化,属激进优化,但字节码码执行效率更高。

可以通过以下虚拟机参数指定Java虚拟机运行时使用的是哪一个即时编译器。

-Client:指定JVM运行在Client模式下,并使用C1编译器

-Server:指定JVM运行在Server模式下,并使用C2编译器

JVM的Client模式和Server模式:

JVM的Server模式启动时,速度较慢,但当运行起来后,性能将会比Client模式更高。原因是JVM在Client模式下使用的是C1编译器(体量较轻)。当JVM运行在Server模式下时,使用的是C2编译器(体量较重)。C2编译器比C1编译器更加彻底,服务运行起来后性能更强,但启动时间长。

我们可以直接在命令行输入 java -verion来查看当前JVM是运行在何种模式下。

如果我们在JVM启动时,没有指定运行在那种模式下,那么虚拟机将会自动检测主机是否为服务器。如果是,则以Server模式运行,否则以Client模式运行。虚拟机检测是否为服务器的标准是,至少有两个CPU和最低2G内存。

不过需要注意的是,如果想切换模式,要先确认JDK是否两种模式都支持。

查看方式是,去JAVA_HOME/jre/bin目录下看是否存在名为Client和Server的目录,存在即表明支持。一般情况下,32位的机器都支持Server模式和Client模式。但64位的JDK只支持Server模式。

C1和C2编译器不同的优化策略:

在不同的编译器上有不同的优化策略。

(1)、C1编译器上主要有方法内联、去虚拟化、冗余消除

①方法内联:将引用的函数代码编译到引用处,这样可以减少栈帧的生成,减少参数传递以及跳转过程。

②去虚拟化:对唯一的实现类进行内联。

③冗余消除:在运行期间把一些不会执行的代码折叠掉

(2)、C2的优化主要在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化:

①标量替换:用标量值替换聚合对象的属性值

②栈上分配:对于未发生逃逸的对象分配内存在栈上而不是堆上。

③同步消除:消除同步操作,通常指消除syhchronized。

分层编译策略:

第一层,为解释执行,在程序解释执行(不开启性能监控)时,可触发第二层。第二层,触发C1编译,可以进行简单优化。第三层,加上性能监控,C2编译器会根据性能监控信息进行激进优化。

不过在现在的Java7版本后,如在启动时指定运行Server模式时,默认将开启分层编译策略,由C1和C2相互协作共同编译任务。

总结:

一般来讲,JIT编译器编译出来的机器码性能比解释器高。C2编译器启动时间比C1编译器长,但C2编译器执行速度远快于C1编译器。

扩展:

JDK9中引入了AOT编译器(静态提前编译器,Ahead Of Time Compiler)

所谓AOT编译,是与即时编译器相对应的概念。在即时编译器中,字节码在程序运行时被转换为可在硬件上执行的本地机器码。而AOT编译,则是在程序执行之前,就直接将字节码编译为本地机器码,在JVM启动时即立即执行。

这样做的好处是:

JVM可以直接执行这些被AOT编译过的代码,无需等待即时编译器的预热,加快了启动速度。

缺点是:

①破坏了Java一次编译,到处运行的特性,必须为每个不同硬件,操作系统编译对应的发行包。

②降低了Java链接过程的动态性,加载的代码在编译期必须全部已知,否则将无法编译通过动态生成class文件的Java代码(如反射等)。

③目前也仅仅支持64的Linux操作系统。

【JVM第七篇】执行引擎的更多相关文章

  1. Java之深入JVM(6) - 字节码执行引擎(转)

    本文为转载,来自 前面我们不止一次的提到,Java是一种跨平台的语言,为什么可以跨平台,因为我们编译的结果是中间代码—字节码,而不是机器码,那字节码在整个Java平台扮演着什么样的角色的呢?JDK1. ...

  2. JVM(6) 字节码执行引擎

    编译器(javac)将Java源文件(.java文件)编译成Java字节码(.class文件). 类加载器负责加载编译后的字节码,并加载到运行时数据区(Runtime Data Area) 通过类加载 ...

  3. JVM之字节码执行引擎

    方法调用: 方法调用不同于方法执行,方法调用阶段唯一任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不执行方法内部的具体过程.方法调用有,解析调用,分派调用(有静态分派,动态分派). 方法解析 ...

  4. 一夜搞懂 | JVM 字节码执行引擎

    前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习字节码执行引擎? 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一 ...

  5. 【JVM之内存与垃圾回收篇】执行引擎

    执行引擎 执行引擎概述 执行引擎属于 JVM 的下层,里面包括 解释器.及时编译器.垃圾回收器 执行引擎是 Java 虚拟机核心的组成部分之一. "虚拟机"是一个相对于" ...

  6. 执行引擎子系统——JVM之五

    一.JVM通过执行引擎来完成字节码的执行,在执行过程中JVM采用的是自己的一套指令系统,每个线程在创建后,都会产生一个程序计数器(pc)和栈(Stack). pc:存放了下一条将要执行的指令: Sta ...

  7. 深入理解JVM虚拟机5:虚拟机字节码执行引擎

    虚拟机字节码执行引擎   转自https://juejin.im/post/5abc97ff518825556a727e66 所谓的「虚拟机字节码执行引擎」其实就是 JVM 根据 Class 文件中给 ...

  8. JVM学习笔记:字节码执行引擎

    JVM学习笔记:字节码执行引擎 移步大神贴:http://rednaxelafx.iteye.com/blog/492667  

  9. 深入理解JVM—字节码执行引擎

    原文地址:http://yhjhappy234.blog.163.com/blog/static/3163283220122204355694/ 前面我们不止一次的提到,Java是一种跨平台的语言,为 ...

随机推荐

  1. Spark核心组件通识概览

    在说Spark之前,笔者在这里向对Spark感兴趣的小伙伴们建议,想要了解.学习.使用好Spark,Spark的官网是一个很好的工具,几乎能满足你大部分需求.同时,建议学习一下scala语言,主要基于 ...

  2. lvs搭建dr负载均衡集群

    一,查看本地centos的版本: [root@localhost lib]# cat /etc/redhat-release CentOS Linux release 8.1.1911 (Core) ...

  3. linux(centos8):禁用selinux(临时关闭/永久关闭)

    一,selinux的用途 1,什么是selinux SELinux:即安全增强型 Linux(Security-Enhanced Linux) 它是一个 Linux 内核模块,也是 Linux 的一个 ...

  4. 通透,23 个问题 TCP 疑难杂症全解析

    每个时代,都不会亏待会学习的人. 在进入今天主题之前我先抛几个问题,这篇文章一共提出 23 个问题. TCP 握手一定是三次?TCP 挥手一定是四次? 为什么要有快速重传,超时重传不够用?为什么要有 ...

  5. 手撸ORM浅谈ORM框架之Add篇

    快速传送 手撸ORM浅谈ORM框架之基础篇 手撸ORM浅谈ORM框架之Add篇 手撸ORM浅谈ORM框架之Update篇 手撸ORM浅谈ORM框架之Delete篇 手撸ORM浅谈ORM框架之Query ...

  6. Jmeter入门(6)- 参数化

    一.什么是参数化 为什么要参数化? 在发送大量的请求时,键对值是写死的,每次请求都需要去修改,无法实现快速添加的需求.想要快速实现该需求,就需要用到参数化. 什么是参数化? 根据需求动态获取数据并进行 ...

  7. 第六章 类(Class) 和对象(Object)

    一.笔记导图 二.实例代码: public class PrintCarStatus{ public static void main(String[] args){ int speed; Strin ...

  8. vue中跳转页面逻辑

    跳转详情页面具体代码 写这个页面需要安装两个 1.安装axios命令 Cnpm install axios --save 2.安装vant Cnpm install vant --save 在inde ...

  9. 如何实现一个FormData

    一.前言 最近项目中遇到一个问题,我们需要在cocos项目里去上传音频文件,而cocos原生环境和平时我们开发所在的浏览器环境和Node环境有很多差异,而cocos环境只提供了基础类,没有提供Form ...

  10. E. Tree Reconstruction 解析(思維)

    Codeforce 1041 E. Tree Reconstruction 解析(思維) 今天我們來看看CF1041E 題目連結 題目 略,請直接看原題 前言 一開始完全搞錯題目意思,還以為每次會刪除 ...