JVM系列(十):执行引擎

1、执行引擎概述

1.1、介绍

  • 执行引擎是Java虚拟机核心的组成部分之一
  • 虚拟机相对于“物理机”,物理机的执行引擎建立在处理器、缓存、指令集和操作系统层面上;虚拟机的执行引擎由软件自行实现,因此能执行不被硬件直接支持的指令集
  • 执行引擎的任务就是将字节码指令解释、编译为对应平台的机器指令

1.2、执行引擎工作过程

  • 执行引擎在执行过程中执行的指令是什么完全依赖于PC寄存器
  • 执行引擎执行完一条指令操作,PC寄存器马上更新下一条要执行的指令
  • 方法在执行过程中,执行引擎可能会通过栈帧局部变量表中的对象引用去定位存储在Java堆区中的实例对象,并通过对象头中的元数据指针定位到目标对象在方法区中的类型信息。
  • 从外观看,所有java虚拟机执行引擎的输入和输出都一致,输入字节码二进制流,处理过程是字节码解析执行,输出的是执行结果。

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

  • 上图中,橙色部分是将源代码转为字节码(前端编译)工作流程,绿色是解释器工作流程,蓝色是JIT即时编译器工作流程。
  • 解释器:Java虚拟机启动时对字节码逐行解释的方法执行,将每条字节码中的内容翻译为对应平台机器指令。
  • JIT即时编译器:当虚拟机发现某些代码块运行频繁,就会将其认定为热点代码(字节码),为了提升热点代码的执行效率,在运行时,由JIT将这些代码编译成本地机器码,并使用各种手段去进行代码优化。
  • Java半编译半解释:
  1. JDK1.0时,Java定位为“解释执行”,只有解释器
  2. 现在Java虚拟机通常将JIT和解释器结合在一起,共同执行

3、机器码、指令、汇编语言

  • 机器码:CPU识别,直接读取(0101)

  • 指令:将特定机器码(0101)序列简化成对应指令(如mov对象文件复制),不同硬件指令不同

  • 指令集:某硬件平台支持的指令集合(X86指令集对应X86架构平台,ARM对应ARM架构平台)

  • 汇编语言:助记符代替机器指令的操作码,用地址符号或标号替代指令操作数地址;不同硬件平台,汇编语言对饮不同机器指令集,通过汇编将其转为机器指令。

  • 字节码

  1. 一种中间状态的二进制文件,比机器码抽象,需要翻译后转为机器码
  2. 字节码与硬件无关

  • C/C++执行过程:


4、解释器和JIT编译器

4.1、解释器

  • JVM当时为了满足Java程序实现跨平台特性,避免采用静态方式(java代码直接转为本地机器指令),先将java源代码转为字节码文件,再通过解释器在运行时逐行解释字节码执行程序

4.2、解释器工作机制

  • 解释器相当于一个运行时的“翻译者”,将字节码中的内容翻译为对应平台的本地机器指令
  • 当一条字节码指令执行完毕,根据PC寄存器中记录的地址取出下一条字节码指令解释执行

4.3、解释器分类:字节码解释器、模板解释器

  • 字节码解释器(古老):执行时通过纯软件代码模拟字节码执行,效率低
  • 模板解释器(目前普遍使用):将每一条字节码和一个模板函数相关联,模板函数直接产生该条字节码执行时的机器码,提高性能。

4.4、Just In Time编译器、即时编译器

  • 基于解释器已经沦为低效的代名词
  • 为了解决这个问题,出现了即时编译器Just In Time编译器、即时编译器)。即时编译目的是避免函数被解释执行,将整个函数体编译为机器代码,每次执行时,仅需要执行编译后的机器码(但是编译也需要时间)
  • 其它编译器:
  1. 编译器:前端编译器(java程序 -> 字节码文件)、JIT编译器(Just in time compiler)、静态提前编译器(AOT,Ahead of time compiler)
  2. JIT编译器亦称后端运行期编译器
  3. 静态提前编译器:直接java程序转为本地机器码,是一种JVM发展方向

4.5、解释器与JIT编译器并存

  • 程序启动后,解释器(一行行解释执行)立马起作用,省去编译时间,立即实行
  • 编译器将代码编译成本地代码,需要一定时间,但是一旦编译为本地代码以后,直接执行本地代码,效率高
  • 程序启动时,如果使用JIT编译器,那么花费时间将字节码都转为本地代码后执行,那样花费时间长,不合适。从服务器角度,重要的是启动后效率提升。所以需要二者平衡。启动时解释器发挥作用,不用等待JIT编译器全部编译后再执行,省去编译时间;随着时间推移,编译起作用,将越来越多的代码编译为本地代码执行,效率提升
  • 当JIT编译器激进优化不成立(简单理解JIT编译失败)时,解释器可以作为JIT编译器的“逃生门”,转而使用解释器执行
  • 当环境内存不足(不足以保存这么多JIT编译的机器指令)时,转而使用解释器

4.6、设置程序执行方式

  • 缺省情况下,使用解释器与即时编译器并存架构
  • -Xint设置完全采用解释器
  • -Xcomp设置完全采用JIT编译器
  • -Xmixed设置采用解释器+JIT编译器的混合模式

4.7、热点代码及探测方式

  • 哪些字节码需要JIT编译为本地机器码,由其执行频率决定
  • 由JIT编译的字节码称为热点代码,或者一个多次被调用的方法、循环次数多的循环体也可成为热点代码
  • JIT针对热点代码作出深度优化,将其直接转为本地机器指令,提升Java性能
  • JIT编译器编译发生在方法的执行过程中,因此也被称为栈上替换,简称OSR(On Stack Replacement)
  • 热点探测方式,方法调用多少次、循环体循环多少次才可以用JIT编译。目前HotSpot VM采用的热点探测方式是基于计数器的热点探索:
  1. 方法调用计数器:统计方法调用次数
  2. 回边计数器:记录循环体循环次数
  • Client模式下默认1500次,Server模式下默认10000次,超过阈值则触发JIT编译,阈值可以通过-XX:CompileThreshold来设置
  • 当方法被调用,先检查是否已经被JIT编译过,如果编译过,直接用缓存的本地代码。没有被编译过,那么方法调用计数器值+1,然后判断方法调用计数器+回边计数器的和是否超过阈值,超过则调用JIT编译,否则使用解释器。

4.8、方法调用计数器

  • 热度衰减:在默认情况下,方法调用计数器统计的不是方法被调用的绝对次数,而是一个相对执行频率,即一段时间内的方法被调用次数。当超过一定时间限度,如果方法的调用次数还不足以将其提交给即时编译器,那么该方法的调用次数要减半。可以通过-XX:CounterHalfLifeTime设置半衰周期时间,单位是秒
  • 热度衰减是在虚拟机进行垃圾回收的时候进行的,可以使用参数-XX:UserCounterDecay参数关闭热度衰减,使用绝对次数。如果关闭热度衰减,只要运行时间够长,大部分方法都会被编译为本地代码

4.9、回边计数器

  • 统计方法中循环体执行的次数,在字节码中,遇到控制流向后跳转的指令称为“回边”,所以建立回边计数器统计的目的是为了触发OSR(栈上替换)编译
  • 虚拟机为客户端情况下,回边计数器计算如下:方法调用计数器阈值(-XX: CompileThreshold)乘以OSR(-XX: OnStackReplacePercentage),结果再除以100。-XX: OnStackReplacePercentage默认值为933。
  • 虚拟机为服务端情况下,回边计数器计算如下:方法调用计数器阈值(-XX: CompileThreshold)乘以OSR(-XX: OnStackReplacePercentage),再减去解释器监控比率(-XX: InterpreterProfilePercentage),得到的结果再除以100。-XX: OnStackReplacePercentage默认值为140,-XX: InterpreterProfilePercentage默认值为33。


5、其它

5.1、HotSpot

  • HotSpot中内嵌两个JIT编译器(或者三个,第三个是JDK10出现的,长期目标是替换C2,叫做Graal编译器),分别是Client CompilerServer Compiler,简称C1和C2

  • Client Compiler:在Client模式下默认使用,对字节码进行简单和可靠的优化,耗时短,达到更快的编译速度,通过以下方法优化:
  1. 方法内联:将引用的函数代码编译到引用处(融合方法),减少栈帧,减少参数传递和跳转过程;

  2. 去虚拟化:对唯一的实现类进行内联(如果一个类没啥事,就合到另一个)

  3. 冗余消除:运行时将不会运行的代码折叠掉;

  • Server Compiler:在Server模式下默认使用,优化时间较长,还有激进优化,编译耗时较短,但是优化后的代码执行效率高,通过以下方法优化:
  1. 标量替换:用标量替换聚合对象的属性值;

  2. 栈上分配:将未逃逸的对象分配在栈上;

  3. 同步消除:清除同步操作,通常指sysnchronized;

5.2、分层编译策略

  • 不开启性能监控,程序解释执行可以触发C1编译,将字节码编译为机器码,进行简单优化
  • 开启性能监控,C2编译根据性能监控信息进行激进优化
  • JDK7之后,如果指定Server模式(64位机子默认Server),默认开启分层编译策略,由C1和C2编译器相互协作执行编译任务
  • C2编译器启动比C1要慢,系统正常开启后,C2的执行速度远快于C1

5.3、Graal编译器,与C2并列

  • JDK10起引入
  • 短短几年,编译效果追平C2
  • 目前还处于实验状态,使用需要参数-XX:UnlockExperimentalVMOptions、-XX:+UserJVMCompiler激活才可以使用

5.4、AOTAhead Of Time)编译器(静态提前编译器),与JIT编译器并列

  • JDK9中引进
  • 与即时编译器相对应,即时编译器是在运行过程中将字节码转为机器码;AOT编译器是在程序运行前字节码转为机器码
  • JDK9引入实验性AOT编译工具jaotc,借助Graal编译器将Java文件转为机器码,并存放到生成的动态共享库中
  • 好处是JAVA虚拟机加载的是编译好的二进制文件,可以直接执行,不必等待JIT编译器的预热,减少Java应用第一次使用运行慢的体验
  • 缺点:
  1. 破坏了Java“一次编译,到处运行”的特性,必须为不同硬件平台编译对应发型包;
  2. 降低了Java链接过程中的动态性,加载的代码在编译期要全部已知;
  3. 还需持续优化,最初仅支持Linux x64 java base

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值