一,深入理解执行引擎
1,执行引擎的概述
在JVM整个体系中,执行引擎属于第三层,主要用来执行具体的字节码文件。本文主要探讨的就是这个执行引擎。
执行引擎是Java虚拟机核心组成的一部分,“虚拟机” 是一个相对于 “物理机” 的一个概念,这两种机器都有执行代码的能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统的层面上的,而虚拟机的执行引擎是由软件自行实现的,因此可以不受物理条件制约的指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的格式。java虚拟机可以理解成一个抽象的计算机,相较于真正的物理机而言,java虚拟机的执行效率会略慢于物理机。
JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统上面,因为字节码指令并非等价于本地机器指令,他内部包含的仅仅是一些能够被JVM识别的字节码指令等信息。如下图所示,这些字节码指令不能直接在操作系统上解释执行,而是需要现通过jvm虚拟机来执行这些字节码指令。
因此,执行引擎的主要作用就是:将字节码指令解释成或者编译成对应平台上面的本地机器指令 ,简单的来说,JVM中的执行引擎充当了将高级语言翻译成机器语言的翻译者。
执行引擎在执行过程中,其需要的具体的字节码指令完全依赖于程序计数器,每当完成一项操作指令之后,程序计数器就会更新下一条需要被执行的指令地址。在方法的执行全过程中,执行引擎有可能会通过存储在局部变量表的对象引用准确的的获取存储在Java堆中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
2,Java代码编译和执行的过程
大部分的程序代码在转换成物理机的目标代码或者虚拟机能执行的指令集之前,都需要经历过几下几个步骤
🧢 前面的黄线流程代表的就是将 .java 文件编译成 .class 文件,属于是前端编译;
🧢 绿色部分属于解释器解释执行的过程,即逐行翻译、解释、执行的过程;
🧢 蓝色部分属于是JIT即时编译器编译性阶段,属于是后端编译。
2.1,解释器和编译器
解释器:当Java虚拟机启动的时候,会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译成对应平台的本地机器指令
JIT编译器:jit,又名Just In Time Compiler , 就是直接将源代码编译成和本地平台相关的机器语言。
在java语言中,是既可以通过解释器来执行代码,也可以通过编译器来执行代码的,这二者都可以达到相同的目的,并且这二者以合作的方式相辅相成,取长补短,以最合适的方法让Java内部执行的效率更高。JVM虚拟机不仅仅是针对于Java语言,只要遵循Jvm虚拟机规范的语言,都可以使用JVM虚拟机解释执行。
如上图,将不同的语言通过统一处理,生成对应的字节码文件,然后通过虚拟机中的解释器或者JIT即时编译器对这些字节码进行解释执行,然后翻译成对应的字节码指令,最后将这些指令全部存储在方法区的CodeCache中。
2.2,机器码、指令、汇编语言、高级语言
1,机器码
各种用二进制编码方式表示的指令,叫做 机器指令码 ,如通过01010101 这种二进制的方式进行编码,最开始人们就用它编写程序,这就是 机器语言。机器语言虽然可以被计算机接收,但是和人们的语言差别太大,不易被人家理解和记忆,用它变成也容易出错。用它编写的程序,一经输入计算机,CPU直接读取运行,因此和其他语言的程序,执行速度最快。机器指令和CPU紧密相关,因此不同类型的CPU所对应的机器指令也就不同。
2,指令
由于机器码是由0和1的二进制组成,可读性实在是太差,于是人们发明了指令。指令就是把机器码特定的 0和1 序列,简化成了对应的指令,如mov和inc等,可读性好。但是由于不同的硬件平台,执行同一个操作,其对应的字节码可能会不同,所以不同硬件平台的同一种指令,对应的机器码也可能不同。在不同的硬件平台,各自支持各自的指令,每个平台所支持的指令总和,称之为对应平台的 指令集。
3,汇编语言
又由于指令的可读性差,于是又发明了这个汇编语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号代替指令或者操作数的地址。在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令,由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别。
4,高级语言
高级语言比上述语言接近人的语言,如当今流行的c或者c++,当计算机执行高级语言的时候,仍然需要把程序解释或者编译成机器指令码,完成这个过程的程序就叫做解释程序或者编译程序。因此不管是汇编语言还是这个高级语言,都需要最终生成这个机器指令,然后将这个机器指令放在CPU上面操作,最终解释执行。
字节码属于是一种中间状态的二进制代码,他比机器码更加抽象,需要直译器转译后才能成为机器码,与硬件环境无关,可以直接通过编译器或者虚拟机器,将源码编译成字节码。
2.3,解释器和编译器工作机制(重点)
解释器真正意义上所承担的角色就是一个 “运行时的翻译者”,就是将字节码中的内容翻译成对应平台的本地机器指令执行。每当一条字节指令被解释执行完成后,接着再根据 程序计数器 中记录的下一条需要被执行的字节码指令执行解释操作。
在JVM平台中,也对解释器进行了优化,采用了一种JIT 的即时编译的技术,目的是避免函数被解释执行,而是将整个函数体编译成机器码,每次函数执行时,只执行编译后的编译码即可,这种方式大大的提升了执行效率。
在hotspot虚拟机中,JIT即时编译器的速度远快于解释器,并且将字节码指令直接生成机器指令,存储在这个方法区的CodeCache中缓存起来,比这个解释器逐行翻译的效率高很多。因此在今天,Java程序的运行性能早以脱胎换骨,已经可以达到和c/c++程序一较高下的地步。
但是即使这个jit即时编译器的速度很快,在HotSpot虚拟机中,依旧保留了这个解释器,原因是JIT即时编译器虽然效率很高,但是需要一定的时间编译成机器码,才能继续工作。但是这个编译器在程序启动之后,可以立马进行工作,省去编译的时间,立即执行。
所以综上两点,在程序启动的时候JIT需要编译,那么就由解释器来执行程序,待JIT即时编译器编译成机器码之后,再由这个JIT即时编译器来完成,这样就能让整个执行引擎发挥最大的效率。因此二者合作共存才能让效率最大化。
2.4,JIT编译器的热点代码和热点探测
Java语言的编译器其实是一段不太确定的操作过程,因为他可能是一指前端编译器(编译器的前端,.java文件编译成 .class文件)的过程,也可能是指后端的编译器(JIT编译器,将字节码转换成机器码)的过程,还有可能是指静态提前编译器,直接把 .java 文件编译成本地机器代码的过程。
在使用这个JIT编译器的时候,需要判断代码被调用执行的频率,对于需要被编译为本地代码的字节码,被称为热点代码 ,JIT编译器在运行时对那些频繁被调用的热点代码会做出深度优化,将其直接编译为对应平台的本地机器指令,以提升Java程序的执行性能。
热点代码 :指的是一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体,都可以被称为"热点代码"。因此可以通过JIT编译器译为本地机器指令,由于这种编译方式发生在方法的执行过程中,因此也被称为栈上替换。
热点探测方式:而是否可以成为这个热点代码,主要是依靠这个热点探测功能,HotSpot虚拟机主要采用的热点探测方式是基于计数器的热点探测。HotSpot虚拟机又将每个方法建立两个不同类型的计数器,分别是方法调用计数器和回边计数器,方法调用计数器用于统计方法的调用次数,回边计数器用于统计循环体的执行次数。
2.5,方法调用计数器和回边计数器
在JIT的热点探测中,主要是通过计数器的方式来实现对代码的探测,计数器主要分为方法调用计数器和回边计数器。
2.5.1,方法调用计数器
这个计数器主要用于统计方法被调用的次数,它的默认阈值在Client模式下是1500次,在Server模式下是10000次,超过这个阈值,就会触发JIT编译。这个阈值也可以通过虚拟机参数 -XX:CompileThreshold进行设置。当一个方法被调用的时候,会先检查这个方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行,如果不存在,则将此方法的调用计数器值加1,然后判断 方法调用计数器和回边计数器 值的和是否超过方法调用计数器的阈值,如果已经超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。
如上图所示,在调用方法时,会先判断该代码是否已经编译,如果已经编译,则直接通过这个JIT即时编译器将机器码生成对应的本地机器码指令;如果未编译,则将方法调用计数器加1,随后回去判断是否超过阈值,如果超过阈值,则会提交编译请求,通过JIT即时编译器进行动态编译,然后将编译后的机器指令缓存在CodeCache中,如果未超过阈值,那么继续通过解释器解释执行。
在JVM内部对调用的次数也做了一定的限制,并不是说一直对调用的次数进行类加,而是在一段时间内记录方法调用的次数,当超过一定的时间限度,如果方法调用的次数依旧没有达到这个阈值,那么方法的调用计数器就会进行一个 衰减 的过程,每次衰减一半,这段衰减的过程被称为方法统计的 半衰周期
进行衰减的动作是虚拟机在垃圾收集的时候顺便进行的,可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭或者开启热度衰减,因此只要系统运行的时间足够长,那么绝大多数的方法都会编译成本地代码。同时也可以通过参数 -XX:CounterHalfLifeTime 设置半衰周期的时间,单位是s
2.5.2,回边计数器
主要是统计一个方法中的循环体的执行次数,在字节码中遇到流控流向后跳转的指令称为 “回边” 。
和方法调用计数器一样,会先判断一下该代码是否已经编译,如果未编译,则回边计数器的值加1,然后去判断将当前累加的值和方法调用计数器的值进行累加是否超过阈值,如果超过,则使用JIT编译器,否则依旧使用解释器执行。
2.6,编译器和解释器设置
上述可知在HotSpot虚拟机中存在解释器和编译器,如通过以下命令可以得知,当前虚拟机采用的是一种混合的方式共同执行程序。
java -version
除了这种之外,也可以通过显式的命令为Java虚拟机指定只由其中一种执行程序,如可以通过以下这个命令设置只使用解释器执行程序
java -Xint -version
或者可以通过以下命令只设置使用编译器来执行程序,但是如果编译出现问题,解释器会接入执行
java -Xcomp -version
当然上面这两种需要在特殊的场景下使用,需要变回混合使用
java -Xmixed -version
除了可以通过这个命令行设置之外,也可以通过这个虚拟机参数就行设置,其代码如下,通过虚拟机的不同参数设置,可以得到以下答案,纯解释需要花8666ms,纯编译只需要花2ms,混合使用也是1-2ms,因此选择这个混合是最佳的,同时也可以知道使用这个纯编译器的时间远远小于这个纯解释型。
/** * * -Xint : 8666ms * -Xcomp:2ms * -Xmixed: 2ms * @author zhenghuisheng * @date : 2023/4/11 */ public class C { public static void main(String[] args) { long start = System.currentTimeMillis(); test(); long end = System.currentTimeMillis(); System.out.println((end - start) + "ms"); } public static void test(){ int k = 0; for (int i = 0; i < 1000000; i++) { for (int j = 0; j < 1000; j++) { k = i + j; } } } }
在虚拟机设置那里修改对应的参数即可。
而在HotSpot虚拟机中内嵌有两个JIT的编译器,分别是Client Compiler和Server Compiler,但是在绝大多数的情况下,这两个编译器被称为C1编译器和C2编译器。
🧢 -client :运行在Client模式下,对字节码进行可靠和简单的优化,耗时短
🧢 -server:运行在Server模式下,对字节码进行耗时长的优化、激进优化,效率更高
C1编译器的优化策略主要有:方法内联、去虚拟化、冗余消除
C2编译器的优化策略主要有:标量替换、栈上分配、同步消除