【目录】 【上一篇:对象的实例化内存布局与访问定位】 【下一篇:String Table】
五、执行引擎
1、执行引擎概述
- 执行引擎是 Java 虚拟机核销的组成部分之一,里面包括解释器、及时编译器、垃圾回收器。
虚拟机
是一个相对于物理机
的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的。而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约的定制指令集与执行引擎的结构体系,能够执行那些不能被硬件直接支持的指令集格式。- JVM 的主要任务是负责装载字节码到其内部,但字节码并不能直接运行在操作系统之上(字节码不等价于本地机器指令),如果想让一个 Java 程序运行起来,则需要执行引擎将字节码指令解释/翻译为对应平台上的本地机器指令才行。
1.1、执行引擎的工作过程:
①、执行引擎在执行过程中究竟需要执行什么样的字节码指令完全依赖于 PC 寄存器;
②、每当执行完一项指令操作之后,PC 寄存器就会更新下一条需要被执行的指令地址;
③、方法在执行过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在 Java 堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
2、Java 代码编译和执行过程
大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过上图中的各个步骤。
Java代码编译是由Java源码编译器(前端编译器)来完成,流程图如下所示:
Java字节码的执行是由JVM执行引擎(后端编译器)来完成,流程图 如下所示:
2.1、什么是解释器?什么是 JIT 编译器?
解释器:当 Java 虚拟机启动时,会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内存”翻译“为对应平台的本地机器执行指令;
JIT 编译器:虚拟机将源码直接编译成和本地机器平台相关的机器语言。
2.2、为什么 Java 是半编译半解释型语言?
JDK1.0时代,将Java语言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
3、机器码、指令、汇编语言
3.1、机器码:
- 机器码就是各种用二进制编码方式表示的指令;用它编写的程序可用被 CPU 直接读取运行;
- 机器指令与CPU紧密相关,所以不同种类的CPU所对应的机器指令也就不同。
3.2、指令:
- 指令就是把机器码中特定的0和1序列,简化成对应的指令(一般为英文简写,如mov,inc等),可读性稍好;
- 由于不同的硬件平台,执行同一个操作,对应的机器码可能不同,所以不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同。
3.3、指令集:
不同的硬件平台,各自支持的指令是有差别的,因此每个平台所支持的指令,被称之为对应平台的指令集。
3.4、汇编语言:
- 用助记符(Mnemonics)代替机器指令的操作码,用<mark地址符号(Symbol)或标号(Label)代替指令或操作数的地址。在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。
- 由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别和执行。
3.5、高级语言:
- 为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言。高级语言比机器语言、汇编语言更接近人的语言
- 当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序或编译程序。
4、解释器
解释器就是一个运行时的”翻译者”,将字节码文件中的内容“翻译“为对应平台的本地机器指令执行,当一条字节码指令被解释执行完成之后,再根据 PC 寄存器中记录的下一条指令执行解释操作。
5、JIT 编译器
在程序运行中,将字节码编译成本地机器码并缓存起来。
💡 概念解释:
Java 语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个 前端编译器,把 .java 文件转换为 .class 文件的过程。也可能是指虚拟机的 后端运行期编译器(JIT编译器、解释器),把字节码转换成本地机器码的过程。还有可能是指使用 静态提前编译器(AOT编译器),直接吧 .java 文件编译成本地机器码的过程。
5.1、解释器与 JIT 编译器的区别:
- JIT 编译器的效率比解释器高;
- 当程序启动后,解释器可以马上发挥做用,省去编译的时间,立即执行;
- 编译器想要发挥作用、把代码翻译成本地指令,需要一定的执行时间,但编译为本地代码之后,执行效率高。
5.2、HotSpot JVM 的执行方式:
当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将由价值的字节码编译为本地机器指令,以换取更高的程序执行效率。
HotSpot 采用的热点探测方式是基于计数器的热点探测:JVM 将会为每一个方法都建立 2 个不同类型的计数器,分别为方法调用计数器(用于统计方法的调用次数)和回边计数器(统计循环体执行的循环次数)
💡 方法调用计数器:
这个计数器主要用于统计方法被调用的次数,默认阈值在 client 模式下是 1500 次,在 server 模式下是 10000 次。方法执行的次数操过这个阈值,就会触发 JIT 编译。
这个阈值可以通过参数 -xx:CompileThreshold 来人为设置。
具体过程:
当一个方法被调用时,会先检查该方法是否存在被 JIT 编译过的版本,如果存在,会优先使用编译后的机器码来执行;如果不存在,则次方法的调用计数器值加 1 ,然后判断方法调用计数器值与回边计数器值之和是否超过方法调用计数器的阈值,如果超过,则将向 JIT 编译器提交一个该方法的代码编译请求。热度衰减:
如果不做任何设置,方法调用计数器统计的不是方法被调用次数的总和,而是一个相对时间内的调用次数。当超过一定的时间限度,如果方法的调用次数任然不足以让它提交给即时编译器编译,那么这个方法的调用计数器值就会被减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称之为此方法的半衰周期。
进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用参数 -XX:-UseCounterDecay 来关闭热度衰减,让方法计数器统计方法调用的绝对次数;也可以使用 -xx:CounterHalfLifeTime 参数来设置半衰周期的时间,单位是秒。
💡 回边计数器:
它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称之为“回边”。
6、AOT 编译器
在程序运行之前,将字节码转换为机器码。
好处:
- Java 虚拟机加载已经预编译成二进制库,可以直接执行。不必等待及时编译器的预热,减少 Ja va应用给人带来“第一次运行慢” 的不良体验。
缺点:
- 破坏了
java 一次编译,到处运行
的理念,必须为每个不同的硬件,OS 编译对应的发行包; - 降低了 Java 链接过程的动态性,加载的代码在编译器就必须全部已知;
- 还需要继续优化中,最初只支持 Linux X64 java base。