jvm-执行引擎

jvm-执行引擎

丁起男 21 2022-11-25

jvm-执行引擎

执行引擎是jvm核心的组成部分之一

虚拟机是相对于物理机的概念,这两种机器都有代码执行能力,其区别是:

  • 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的
  • 虚拟机的执行引擎规则是由软件自行实现的

因此可用不受物理条件制约的指定指令集与执行引擎的结构体,能够执行哪些不被硬件直接支持的指令集格式

jvm的主要认为是负责装载字节码到其内部,但字节码并不能直接运行在操作系统上,那么想要让一个java程序运行起来,就需要执行引擎将字节码指令解释/编译成对应平台上的本地机器指令才可以。简单来说,就是执行引擎充当了将高级语言转化成机器语言的翻译者

java的执行引擎是半解释半编译的(早期是解释执行的):

  • 解释器:当jvm启动时会根据预定义的规范堆字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令
  • jit编译器:就jvm将源代码直接编译成和本地机器平台相关的机器语言

工作流程

  1. 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于pc寄存器
  2. 每当执行完一项指令后,pc寄存器会更新下一条需要被执行的指令地址

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

从外观上看,所有的jvm的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果

解释器

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

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

再java的发展历史里,一共有两套解释执行器

  • 字节码解释器:通过单纯的软件代码模拟字节码的执行,效率低下
  • 模板解释器:将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,从而大幅提高性能

解释器以及沦落为低效的代名词,为了解决这个问题,jvm支持一种叫做即使编译的即使。目标是为了避免函数被解释执行,而是将整个函数体编译成机器码,每次函数执行时,只执行编译后的机器码即可

为什么保留解释器

  • 响应速度快:当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行;编译器想要发挥作用,把代码编译成本地代码,需要一定的执行时间
  • 解释器在编译器进行激进优化不成立的时候,作为编译器的“逃生门”

jit编译器

hotspot采用解释器和即时编译器并存的架构。再java虚拟机运行时,解释器和即使编译器能够互相协作,各自取长补短,尽力去选择合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间

hostspot执行方式

  • 当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即使编译器全部编译器全部编译完成再执行,这样可以省去不必要的编译时间
  • 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的执行效率

jRockit内部就不保护解释器

hotspot也可以通过指令完全采用解释器或者编译器:

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

hotspot内置了两个jit编译器:

  • -client:指定jvm在Client模式下,并使用c1编译器。c1编译器会对字节码进行简单和和靠的优化,耗时短
  • -server:指定jvm在server模式下,并使用c2编译器。c2编译器进行耗时较长的优化,以及激进的优化。但代码执行效率更高

64位的系统只能使用-server,并且无需指定

编译器分类

  • 前端编译器:把java文件转变为class文件
  • 后端编译器(jit):把字节码转变为机器码
  • 静态提前编译器(aot):直接把java文件编译成本地机器代码

热点探测

是否需要启动jit编译器将字节码编译为对应平台的机器指令,需要根据代码被调用的频率而定。关于哪些需要被编译为本地机器指令的代码,被称为热点代码,jit编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的机器指令,以此提升java程序的执行性能

一个被多次调用的方法,或者是一个方法内部循环较多的循环体都可以被称之为“热点代码”,因此都可以通过jit编译器编译为本地机器指令。由于这种编译方式在方法的执行过程中,因此也被称为栈上替换(osr),

目前hotspot采用的热点探测方式,是基于计数器的热点探测 。hotspot为每个方法都创建2个不同类型的计数器:

  • 方法调用计数器:统计方法的调用次数

    它的默认阈值在clinet模式下是1500次,在server模式下是10000次。超过这个阈值,就会触发jit编译(可以通过参数-XX:CompileThreshold设置)

    热度衰减:当超过一定时间,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那么这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称之为方法统计的半衰期(可以通过参数-XX:UseCounterDecay来关闭衰减,也可以通过-XX:CounterHalfLifeTime设置半衰期周期,单位是秒)

  • 回边计数器:统计循环体执行的循环次数

    在字节码中遇到控制流向后挑的指令称之为回边。显然建立回边计数器统计的目的就是为了触发osr编译

c1、c2编译器优化策略

c1:

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

c2主要是在全局层面,逃逸分析是优化的基础:

  • 标量替换:用标量值替换聚合对象的属性值
  • 栈上分配:对与未逃逸的对象分配在栈上
  • 同步消除:清除同步操作,通常指synchronized

分层编译

程序解释执行(不开窍性能监控)可以触发c1编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,c2编译会根据性能监控信息进行激进优化

java1之后,默认开启分层编译

aot编译器

jdk9引入了aot编译器(静态提前编译器)

java9引入了实验性aot编译工具jaotc,它借助了graal编译器,将所输入的java类文件转换为机器码,并存放至生成的动态共享库中

所谓aot编译,是与jit编译相对立的一个概念

  • jit编译器:在程序的运行过程中,将字节码转换为机器码
  • aot编译器:在程序运行之前,将字节码转换为机器码

优点:

  • 可以不必等待即时编译器的预热

缺点:

  • 破坏了java一次编译到处运行,必须为每个不同的硬件、os编译对应的发行包
  • 降低了java连接过程的动态性,加载的代码在编译器就必须全部已知
  • 目前只支持Linux x64