执行引擎概述

执行引擎 - 图1

执行引擎是Java虚拟机核心的组件之一

虚拟机是一个相对于物理机的概念

物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的

但是虚拟机的执行引擎是由软件自行实现的,但是它的效率还是不如物理机

JVM的主要任务是装载字节码到其内部,由执行引擎翻译为物理机器能够识别的本地机器指令,由本地机器去执行

简单来说,它就是一个翻译官

执行引擎 - 图2

Java代码编译和执行过程

解释器 和 JIT编译器

解释器:当Java虚拟机启动的时候,会根据预定义的规范对字节码采用逐行解析的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令,然后去执行

JIT编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言,并不马上执行

执行引擎 - 图3

解释器就是图中绿色的部分,JIT编译器就是图中蓝色的部分

对于我们Java来说,两个都有

所以因为这个所以Java才叫做半编译半解释型语言,因为我们的Java既可以使用解释器,也可以使用编译器

机器码、指令、汇编语言

机器码:采用二进制编码方式来表示的指令叫做机器指令,机器语言虽然能被计算机理解,但是对人来说不友好

指令:将机器码中特定的指令简化,比如mov,可读性稍好,但是对于不同的硬件平台不一样,所以指令也不同

指令集:不同硬件平台支持不同的指令,一个平台的全部指令就是指令集。很明显各个平台有自己的指令集

汇编语言:指令集可读性还是太差,所以人们发明了汇编语言,汇编需要翻译为机器指令才能被机器执行

高级语言:汇编还是不太容易,所以人们发明了可读性更好的高级语言

执行引擎 - 图4

字节码

字节码是一种中间状态的二进制代码,比机器码更加抽象,需要直译器才能翻译为机器码

字节码主要是为了实现特定软件运行和软件环境,和硬件无关

解释器

在Java的历史中,一共有两套解释器,也就是古老的字节码解释器,现在普遍使用的模板解释器

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

而模板解释器将每一条字节码和一个模板函数相关联,很大程度上提高了解释器的性能

但是我们现在,解释器已经成为了一个低效的代名词,并且时常被C/C++程序员调侃,但是他还是有存在的意义

JIT编译器

概念

为了解决效率低下的问题,JVM支持一种叫做即时编译的技术,即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译之后的机器码即可,这也就是我们的JIT编译器

JIT本身就是Just In Time,有即时的意思,所以我们在讲的时候不要讲JIT即时编译器,只需要说JIT编译器

对于HotSpot VM,它是目前市面上高性能的代表作之一,它采用解释器和即时编译器并存的架构,可以互相协作取长补短

使用JIT编译器他的速度快,因为有缓存。但是解释器也有好处,就是解释器可以马上发生作用,马上运行,响应速度快

当虚拟机启动的时候,解释器可以首先发挥作用,而不必等着JIT编译器全部编译之后才完成,可以省去很多不必要的编译时间

而且随着时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译成为本地机器指令,来换取更好的程序执行效率

JRockit VM 其实就是将解释器砍掉了,那么换句话说,JRockit就是将快速响应砍掉了,而去注重运行效率


热点探测

概念解释

Java语言的编译期其实有多个

  • 一个是前端编译期,也就是我们前端编译器将.java文件变为.class文件的过程
  • 一个是后端编译期,也就是我们后端运行期编译器的JIT编译器将字节码转换为机器码的过程
  • 一个是静态提前编译期,静态提前编译器直接将.java翻译为机器码的过程

前端编译器:Sun 的 Javac、Eclipse JDT中的ECJ

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

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

热点探测

热点代码和探测方式,其实就是我们代码的执行频率,假如我们代码执行频率高,那么就叫做热点代码

JIT编译器在运行时会针对那些频繁被调用的热点代码进行深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能

那么我们说热点代码,什么程度叫做执行频率高呢?我们依靠热点探测功能

我们HotSpot VM所采用的热点探测方式是基于计数器的热点探测

  • 方法调用计数器:统计方法的调用次数
  • 回边计数器:统计循环体执行的循环次数

方法调用计数器

默认阈值在Client模式下是1500次,在Server模式下是10000次,可以通过-XX:CompileThreshold设定

超过这个模式就是JIT的热点代码,编译后的代码会被放到方法区中去

执行引擎 - 图5

热度衰减

假如不做任何的设置,那么只要程序执行的时间够长,所有的方法都会被JIT缓存上,这显然不是我们想要看到的

所以有一个热度衰减技术,它是判断一段时间范围内的次数,超过了这个时间,那么就减少一半,这个时间就被称为半衰周期

那么我们的半衰周期可以使用-XX:UseCounterDecay来关闭热度衰减

可以使用-XX:CounterHalfLifeTime设置半衰周期的时间,单位是秒

回边计数器

回边计数器就是统计循环体内执行的次数


HotSpot VM 可以设置程序执行的方式

这个就是说我们可以设置HotSpot是使用解释器还是JIT编译器,默认那就是混合使用的方式,但是我们可以通过命令调节

  • -Xint:完全采用解释器
  • -Xcomp:完全采用即时编译器模式执行程序,假如即时编译出现问题,解释器会介入执行
  • -Xmixed:采用解释器+即时编译混合模式共同执行程序,也是默认情况

执行引擎 - 图6

C1、C2

HotSpot VM中内置了两个JIT编译器,分别为 Client Compiler和Server Compiler,大部分我们简称为C1编译器、C2编译器

开发人员可以通过显式指定Java虚拟机在运行时到底使用哪一种即时编译器

  • -client:指定Java虚拟机运行在Client模式下,并且使用C1编译器
  • -server:指定Java虚拟机运行在Server模式下,并且使用C2编译器

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

我们的64位操作系统,它就是C2编译器,即便设置了C1也会忽略掉,使用C2

C1、C2不同的优化策略

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

  • 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递和跳转的过程
  • 去虚拟化:对唯一的实现类进行内联
  • 冗余消除:在运行期间把一些不会执行的代码折叠掉

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

  • 标量替换:用标量值代替聚合对象
  • 栈上分配:对未逃逸的对象分配在栈而不是在堆
  • 同步消除(同步省略):清除同步操作,主要指的是synchronized

C2编译器的这些内容其实我们之前就已经说过了 我们还说过目前HotSpot用的技术主要是标量替换和同步消除,栈上分配还没有真正用到 但是当时没有讲只能在server模式下,使用C2才可以使用,现在补上

分层编译策略

虽然说x64系统就算使用命令开启C1也会忽略掉,但是这不是说只能使用C2不能使用C1

程序解释执行,不开启性能监控,将字节码编译为机器码,可以进行简单优化,也可以加上性能监控,C2编译会根据性能监控进行激进优化

只不过在JDK7版本之后,一旦开发人员在程序中显示指定命令-server时,默认会开启分层编译策略,由C1和C2编译器相互协作共同执行编译任务

一般来讲,JIT编译出来的机器码性能比解释器高

C2编译器启动时间又比C1时间长,系统稳定之后,C2编译器执行速度远远高于C1编译器

Graal 编译器 和 AOT编译器

在JDK10,HotSpot又加入了一个全新的即时编译器:Graal编译器,甚至发展出了Graal虚拟机,说以后Graal虚拟机替代HotSpot虚拟机,完全不是一个玩笑

Graal编译器在短短几年就已经追平了C2编译器

Graal和我们G2是平等的概念,AOT编译器是和我们的JIT平等的概念

我们知道即时编译器是在程序的运行过程中,将字节码转换为机器码,并部署至托管环境中的过程。

而AOT编译指的是在程序运行之前就将字节码转换为机器码的过程