彻底认识「JIT编译器的运行原理」|Java 开发实战

简介: 彻底认识「JIT编译器的运行原理」|Java 开发实战

前提概要


解释器


Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”(hotspot code)。正因为如此,我们的hotspot的虚拟机就是因此而得名。



解释器优点


  • (占用空间较少)解释执行占用更小的内存空间
  • (启动和首次执行速度较快)当程序需要迅速启动的时候,解释器可以首先发挥作用,省去了编译的时间,立即执行
  • (提高动态性和移植性)当处于程序的动态效果下,如果预先编译好所有相关的静态本地代码后,就无法实现动态化扩展,以及提高移植到其他计算机平台架构下的能力

编译器


为了提高热点代码的执行效率,在运行时,即时编译器(Just In Time Compiler,下文称 JIT编译器 )会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。


编译器优点


  • (提高运行速度)在程序运行时,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获得更高的执行效率。
  • (逆转优化)同时,当编译器进行的激进优化失败的时候,还可以进行逆优化来恢复到解释执行的状态。



因此,整个虚拟机执行架构中,解释器与编译器经常配合工作,如下图所示。

image.png


解释器与编译器并存的架构(流程)


  1. 如果Java程序需要迅速启动和执行时,或者只是执行一次,解释器可首先发挥作用,省去编译时间,立即执行程序运行后,随着时间推移,JIT编译器逐渐发挥作用,把越来越多的代码编译成本地代码后,可获取更高执行效率。
  2. 程序运行环境中内存资源限制较大(如部分嵌入式系统中),可使用解释执行节约内存,反之可使用JIT编译执行提升效率
  3. 解释器还可作为JIT编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立时可通过逆优化(Deoptimization)退回到解释状态继续执行



故,在整个虚拟机执行架构中解释器与编译器经常配合工作


  • Xint设置:用户可以使用参数 -Xint 强制虚拟机运行于 “解释模式”(Interpreted Mode),这时候编译器完全不介入工作。
  • -Xcomp设置:强制虚拟机运行于 “编译模式”(Compiled Mode),这时候将优先采用编译方式执行,但是解释器仍然要在编译无法进行的情况下接入执行过程
  • -Xmixed设置:这种配合使用的方式称为“混合模式”(Mixed Mode)



通过虚拟机 -version 命令可以查看当前默认的运行模式。





即时编译器(JIT编译器)


JIT编译器不是虚拟机的必需部分,但JIT编译器编译性能的好坏、代码优化程度的高低是衡量一款商用虚拟机优秀与否的最关键的指标之一,也是虚拟机中最核心且最能体现虚拟机技术水平的部分





被编译对象和触发条件


在运行过程中会被即时编译的“热点代码”有两类,即:


编译的目标对象


  • 被多次调用的方法
  • 编译器会将整个方法作为编译对象,这也是标准的JIT 编译方式
  • 被多次执行的循环体
  • 由循环体出发的,但是编译器依然会以整个方法作为编译对象,因为发生在方法执行过程中,称为栈上替换

判断热点代码


「判断一段代码是否是热点代码,是不是需要出发即时编译」这样的行为称为热点探测(Hot Spot Detection),探测算法有两种,分别为



基于采样的热点探测(Sample Based Hot Spot Detection)


虚拟机会周期的对各个线程栈顶进行检查,如果某些方法经常出现在栈顶,这个方法就是“热点方法”


  • 优点:实现简单、高效,很容易获取方法调用关系。
  • 缺点:很难确认方法的reduce(衰减),容易受到线程阻塞或其他外因扰乱

基于计数器的热点探测(Counter Based Hot Spot Detection)


为每个方法(甚至是代码块)建立计数器,执行次数超过阈值就认为是“热点方法”

  • 优点:统计结果精确严谨。
  • 缺点:实现麻烦,不能直接获取方法的调用关系



HotSpot使用的是第二种-基于技术其的热点探测,并且有两类计数器:


  • 方法调用计数器(Invocation Counter )
  • 回边计数器(Back Edge Counter )

两个即时编译器


从上面的解释器和编译器的协同合作架构图中,应该可以了解到,JVM虚拟机实现了两个不同的JIT编译器,分别称为 Client Compiler和 Server Compiler ,或者简称为 C1 编译器和 C2 编译器



热点触发的阈值


这两个计数器都有一个确定的阈值,超过后便会触发JIT编译,具体细节和内容下面会详细讲述。

上面提到了一下两种热点探测的计数器:


方法调用计数器(Invocation Counter )


  • 首先是方法调用计数器:
  • Client模式下默认阈值是1500 次。
  • Server 模式下是 10000次。
  • 这个阈值可以通过 -XX:CompileThreshold 来人为设定。


  • 如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内的方法被调用的次数。(可以理解为滑动窗口)。
  • 当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那么这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就成为此方法的统计的半衰期( Counter Half Life Time)。
  • 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 -XX:CounterHalfLifeTime 参数设置半衰周期的时间 (时间窗口秒),单位是秒。整个 JIT 编译的交互过程如下图。


image.png




回边计数器(Back Edge Counter )


  • 作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”( Back Edge )。
  • 显然,建立回边计数器统计的目的就是为了触发 OSR 编译。关于这个计数器的阈值, HotSpot 提供了 -XX:BackEdgeThreshold 供用户设置。


但是当前的虚拟机实际上使用了 -XX:OnStackReplacePercentage  来简介调整阈值,计算公式如下:

image.png



  • Client模式, 公式为方法调用计数器阈值(CompileThreshold)X OSR 比率(OnStackReplacePercentage)/100 。其中OSR比率默认为933,那么,回边计数器的阈值为13995
  • Server模式,公式为方法调用计数器阈值(Compile Threashold)X (OSR (OnStackReplacePercentage)- 解释器监控比率 (InterpreterProfilePercent))/100



其中onStackReplacePercentage 默认值为 140,InterpreterProfilePercentage 默认值为 33,如果都取默认值,那么 Server 模式虚拟机回边计数器阈值为 10700 。




编译过程


默认情况下,无论是方法调用产生的即时编译请求,还是OSR请求,虚拟机在代码编译器还未完成之前,都仍然将按照解释方式继续执行,而编译动作则在后台的编译线程中进行。


用户可以通过参数 -XX:-BackgroundCompilation来禁止后台编译,这样,一旦达到 JIT 的编译条件,执行线程向虚拟机提交便已请求之后便会一直等待,直到编译过程完成后再开始执行编译器输出的本地代码。




虚拟机运行模式


目前的HotSpot编译器默认的是解释器和其中一个即时编译器配合的方式工作,具体是哪一个编译器,取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与计算机的硬件性能自动选择运行模式,用户也可以使用 -client 和 -server 参数强制指定虚拟机运行在 Client 模式或者 Server 模式。



Client Compiler(了解即可) :

image.png



它是一个简单快速的三段式编译器,主要关注点在于局部的优化,放弃了许多耗时较长的全局优化手段。


  • 第一阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(High-Level Intermediate Representaion , HIR)。在此之前,编译器会在字节码上完成一部分基础优化,如 方法内联,常量传播等优化。


  • 第二阶段,一个平台相关的后端从 HIR 中产生低级中间代码表示(Low-Level Intermediate Representation ,LIR),而在此之前会在 HIR 上完成另外一些优化,如空值检查消除,范围检查消除等,让HIR 更为高效。


  • 第三阶段,在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在 LIR 上分配寄存器,做窥孔(Peephole)优化,然后产生机器码。




Server Compiler(了解即可):


面向服务端典型应用并为服务端性能配置特别调整过的编译器也是一个充分优化过的高级编译器,几乎能达到GNU C++编译器使用-02参数时的优化强度会执行所有经典的优化动作。


  • 无用代码消除(Dead Code Elimination)、
  • 循环展开(LoopcUnrolling)、
  • 循环表达式外提(Loop Expression Hoisting)、
  • 消除公共子表达式(Common Subexpression Elimination)、
  • 常量传播(Constant Propagation)、
  • 基本块重排序(Basic Block Reordering)等


还会实施一些与Java语言特性密切相关的优化技术,如

  • 范围检查消除(Range Check Elimination)、
  • 空值检查消除(Null Check Elimination)等


还可能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的激进优化,如

  • 守护内联(Guarded Inlining)、
  • 分支频率预测(Branch Frequency Prediction)等


  • Server Compiler的寄存器分配器是一个全局图着色分配器,它可充分利用某些处理器架构(如RISC)上的大寄存器集合


编译速度远超传统静态优化编译器,相对Client Compiler代码质量有所提高,可减少本地代码执行时间,从而抵消额外的编译时间开销



如何从外部观察即时编译器的编译过程和编译结果?


  • -XX:+PrintCompilation 在即时编译时,打印被编译成本地代码的方法名称


  • -XX:+PrintInlining 在即时编译时,输出方法内联信息


  • -XX:+PrintAssembly 在即时编译时,打印被编译方法的汇编代码,虚拟机需安装反汇编适配器HSDIS插件,Product版虚拟机需加入参数-XX:+UnlockDiagnosticVMOptions打开虚拟机诊断模式


  • -XX:+PrintOptoAssembly 用于Server VM,输出比较接近最终结果的中间代码表示,不需HSDIS插件支持


  • -XX:+PrintLIR 用于Client VM,输出比较接近最终结果的中间代码表示,不需HSDIS插件支持


  • -XX:+PrintCFGToFile 用于Client Compiler,将编译过程中各阶段数据(如,字节码、HIR生成、LIR生成、寄存器分配过程、本地代码生成等)输出到文件中


  • -XX:PrintIdealGraphFile 用于Server Compiler,将编译过程中各阶段数据(如,字节码、HIR生成、LIR生成、寄存器分配过程、本地代码生成等)输出到文件中



注,要输出CFG或IdealGraph文件,需Debug或FastDebug版虚拟机支持,Product版的虚拟机无法输出这些文件














相关文章
|
3天前
|
消息中间件 Java 应用服务中间件
JVM实战—1.Java代码的运行原理
本文介绍了Java代码的运行机制、JVM类加载机制、JVM内存区域及其作用、垃圾回收机制,并汇总了一些常见问题。
JVM实战—1.Java代码的运行原理
|
8天前
|
安全 Java 程序员
《从头开始学java,一天一个知识点》之:控制流程:if-else条件语句实战
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问"`a==b`和`equals()`的区别",大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列为你打造Java「速效救心丸」!每天1分钟,地铁通勤、午休间隙即可完成学习。直击高频考点和实际开发中的「坑位」,拒绝冗长概念,每篇都有可运行的代码示例。明日预告:《for与while循环的使用场景》。 ---
51 19
|
10天前
|
Oracle Java 关系型数据库
课时37:综合实战:数据表与简单Java类映射转换
今天我分享的是数据表与简单 Java 类映射转换,主要分为以下四部分。 1. 映射关系基础 2. 映射步骤方法 3. 项目对象配置 4. 数据获取与调试
|
10天前
|
存储 Java 编译器
课时11:综合实战:简单Java类
本次分享的主题是综合实战:简单 Java 类。主要分为两个部分: 1.简单 Java 类的含义 2.简单 Java 类的开发
|
2月前
|
存储 缓存 Java
Java中的分布式缓存与Memcached集成实战
通过在Java项目中集成Memcached,可以显著提升系统的性能和响应速度。合理的缓存策略、分布式架构设计和异常处理机制是实现高效缓存的关键。希望本文提供的实战示例和优化建议能够帮助开发者更好地应用Memcached,实现高性能的分布式缓存解决方案。
50 9
|
3月前
|
Java
Java基础却常被忽略:全面讲解this的实战技巧!
本次分享来自于一道Java基础的面试试题,对this的各种妙用进行了深度讲解,并分析了一些关于this的常见面试陷阱,主要包括以下几方面内容: 1.什么是this 2.this的场景化使用案例 3.关于this的误区 4.总结与练习
|
3月前
|
Java 程序员
Java基础却常被忽略:全面讲解this的实战技巧!
小米,29岁程序员,分享Java中`this`关键字的用法。`this`代表当前对象引用,用于区分成员变量与局部变量、构造方法间调用、支持链式调用及作为参数传递。文章还探讨了`this`在静态方法和匿名内部类中的使用误区,并提供了练习题。
60 1
|
4月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
4月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
120 8
|
Java 开发工具 git
Java开发初级6.24.3
5.在Git使用过程中,进行Git配置的操作命令是哪个() A. config B. config -g C. config -a D. git config 相关知识点: 在git中,经常使用git config 命令用来配置git的配置文件,git配置级别主要有:仓库级别 local 【优先级最高】、用户级别 global【优先级次之】、系统级别 system【优先级最低】 正确答案:D 10.RDBMS是什么? A. Rela Database Management Systems B. Relational Database Management Systems C. Relation
152 0

热门文章

最新文章