即时编译器在JVM调优战场的决胜策略

2024-03-22 19:20

本文主要是介绍即时编译器在JVM调优战场的决胜策略,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

        

目录

一、方法内联

二、循环展开

三、分支预测

四、逃逸分析

        4.1 栈上分配

        4.2 标量替换

        4.3 同步消除

五、冗余消除


        JVM中的即时编译器(如HotSpot的C1、C2编译器)会对代码进行即时编译优化,即时编译优化(Just-In-Time Compilation Optimization)是Java虚拟机(JVM)为了提升运行时性能而采取的一种策略。

        即时编译优化主要包括以下几个方面,下面来详细介绍一下

一、方法内联

        目前主流的商用Java虚拟机里,Java程序都是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行优化,被称为方法内联。

        通过这种方式来减少栈帧的创建和销毁以及参数传递等操作,从而减少方法调用的开销,提高性能。

        然而,方法内联并非总是有益的,如果内联后的代码量过大可能会导致内存占用增加,并且会降低JIT编译速度。因此,JIT编译器通常会根据一些策略判断是否以及如何进行内联,例如考虑方法大小、调用频率、方法体复杂度等因素。

二、循环展开

        循环展开是一种代码优化技术,它发生在Java虚拟机(JVM)在运行时将热点代码(即频繁执行的字节码)转化为机器码的过程中。循环展开的具体做法是将循环体内的部分或全部迭代复制多次,从而减少循环控制结构的开销。

        例如,原始的循环代码可能是这样的:

for (int i = 0; i < loopCount; i++) {// 执行某些操作...
}

        经过循环展开后,可能变成如下形式:

int unrolledLoopCount = loopCount / UNROLL_FACTOR;
for (int i = 0; i < unrolledLoopCount; i++) {// 执行某些操作...// 执行相同的操作...// ...重复UNROLL_FACTOR次
}// 如果loopCount不能整除UNROLL_FACTOR,则处理剩余迭代次数
if (loopCount % UNROLL_FACTOR != 0) {for (int i = unrolledLoopCount * UNROLL_FACTOR; i < loopCount; i++) {// 执行剩余的迭代次数}
}

        循环展开带来的潜在优势:

  1. 降低循环控制的开销:减少了判断循环条件和更新循环变量的操作。
  2. 提高指令级并行性:对于支持多核处理器和超标量架构的系统来说,展开后的循环可以提供更多的并发执行机会。
  3. 增强缓存局部性:当循环体内的操作涉及到内存访问时,循环展开有可能提高数据在CPU缓存中的命中率,从而加速执行速度。

        过度的循环展开可能导致代码膨胀、寄存器压力增大以及缓存效率下降等问题,因此编译器通常会根据循环的特征、目标平台的特点以及性能分析结果来智能地选择是否以及如何展开循环。

三、分支预测

        即时编译的分支预测优化是 Java 虚拟机(JVM)在运行时对热点代码进行性能优化的一种技术,特别是在即时编译器(Just-In-Time Compiler, JIT)阶段。分支预测是一种处理器级别的策略,用于猜测程序中的条件分支指令可能执行的路径,并预先加载相应的指令序列以减少实际跳转带来的延迟。

        在即时编译中,编译器不仅会考虑 CPU 硬件的分支预测功能,还会根据已有的运行信息和特定算法来进行更精确的分支预测优化。

四、逃逸分析

        逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里被定义后,他可能被外部方法引用,例如作为调用参数传递到其他方法,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。

        如果能证明一个对象不会逃逸到方法或线程外,或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化。

        4.1 栈上分配

        在 Java 虚拟机中,Java 堆上分配创建对象的内存空间几乎是常识,Java 堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。

        虚拟机的垃圾收集子系统会回收堆中不在使用的对象,但回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。

        如果确定一个对象不会逃逸出线程之外,那让对象在栈上分配将会是一个不错的选择,对象所占用的内存空间就可以随栈帧出栈而销毁。

        在一般应用中完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁,垃圾收集子系统的压力会下降很多。栈上逃逸可以支持方法逃逸,但不支持线程逃逸。

        4.2 标量替换

        若一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据(int、long等数值类型及reference类型等)都不能在进一步分解了,那么这些数据就可以被称为标量。

        相对的,如果一个数据可以继续分解,被称为聚合量,Java对象就是典型的聚合量。如果把一个Java对象拆散,根据程序访问情况,将其用到的成员变量恢复为原始类型来访问,这个过程称为标量替换。

        假如逃逸分析能证明一个对象不会被方法外部访问,并且这个对象可以拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干被这个方法使用的成员变量来替代。

        将对象拆分后,除了可以让对象的成员变量在栈上分配和读写外,还可以为后续进一步优化创建条件。标量替换可以视作栈上分配的一种特例,实现更简单,但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。

        Jdk6 才开始初步支持逃逸分析。一直到 Jdk7 时这项优化才成为服务端编译器默认开启的选项。如果需要,或者确认对程序运行有益,用户可以使用参数 --XX:+DoEscapeAnalysis 来手动开启逃逸分析,开启之后可以通过参数 -XX:PrintEscapeAnalysis 来查看分析结果。有了逃逸分析之后,可以使用参数 -XX:EliminateAllocations 来开启标量替换,使用 +XX;EliminateLocks 来开启同步消除。

        4.3 同步消除

        线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对于这个变量实施的同步措施也就可以安全地消除掉。

五、冗余消除

        冗余消除是一种优化技术,用于减少生成的机器码中的不必要的重复计算和指令。在运行时,JIT 编译器会对热点代码(即频繁执行的代码段)进行分析,并尝试找出那些可以提前计算、存储结果并在后续执行中复用的情况,或者发现那些总是产生相同结果的条件检查和分支等。

        冗余消除的具体形式包括:

  • 循环不变量外提:将循环内部不会变化的计算移动到循环外部,避免每次迭代都进行相同的计算。
  • 常量折叠与代数简化:识别出已知的常量表达式并预先计算它们的结果,或对代数表达式进行简化。
  • 死代码删除:移除那些无论如何都不会被执行到的代码。
  • 公共子表达式消除:如果一个表达式的值已经被计算过,那么之后再次遇到该表达式时就可以直接使用之前的结果,而不是重新计算。
  • 数组边界检查消除:对于确定安全的数组访问,编译器可以消除每次数组元素访问时的边界检查。

        通过这些冗余消除技术,即时编译器能够提高程序的运行效率,减少CPU周期和内存访问次数,进而提升应用程序的整体性能。

        总结,即时编译优化对于 Java 应用的性能至关重要,尤其是针对长时间运行且有大量热点代码的应用场景,通过合理配置 JVM 参数及深入了解即时编译机制,可以在一定程度上有效提升系统整体性能。

往期经典推荐

JVM垃圾收集器你会选择吗?-CSDN博客

JVM内存模型深度解读-CSDN博客

一文看懂Nacos如何实现高效、动态的配置中心管理_nacos配置中心-CSDN博客

TiDB内核解密:揭秘其底层KV存储引擎如何玩转键值对-CSDN博客

领航分布式消息系统:一起探索Apache Kafka的核心术语及其应用场景-CSDN博客

这篇关于即时编译器在JVM调优战场的决胜策略的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/836034

相关文章

Java方法重载与重写之同名方法的双面魔法(最新整理)

《Java方法重载与重写之同名方法的双面魔法(最新整理)》文章介绍了Java中的方法重载Overloading和方法重写Overriding的区别联系,方法重载是指在同一个类中,允许存在多个方法名相同... 目录Java方法重载与重写:同名方法的双面魔法方法重载(Overloading):同门师兄弟的不同绝

Spring配置扩展之JavaConfig的使用小结

《Spring配置扩展之JavaConfig的使用小结》JavaConfig是Spring框架中基于纯Java代码的配置方式,用于替代传统的XML配置,通过注解(如@Bean)定义Spring容器的组... 目录JavaConfig 的概念什么是JavaConfig?为什么使用 JavaConfig?Jav

Java数组动态扩容的实现示例

《Java数组动态扩容的实现示例》本文主要介绍了Java数组动态扩容的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1 问题2 方法3 结语1 问题实现动态的给数组添加元素效果,实现对数组扩容,原始数组使用静态分配

Java中ArrayList与顺序表示例详解

《Java中ArrayList与顺序表示例详解》顺序表是在计算机内存中以数组的形式保存的线性表,是指用一组地址连续的存储单元依次存储数据元素的线性结构,:本文主要介绍Java中ArrayList与... 目录前言一、Java集合框架核心接口与分类ArrayList二、顺序表数据结构中的顺序表三、常用代码手动

JAVA项目swing转javafx语法规则以及示例代码

《JAVA项目swing转javafx语法规则以及示例代码》:本文主要介绍JAVA项目swing转javafx语法规则以及示例代码的相关资料,文中详细讲解了主类继承、窗口创建、布局管理、控件替换、... 目录最常用的“一行换一行”速查表(直接全局替换)实际转换示例(JFramejs → JavaFX)迁移建

Spring Boot Interceptor的原理、配置、顺序控制及与Filter的关键区别对比分析

《SpringBootInterceptor的原理、配置、顺序控制及与Filter的关键区别对比分析》本文主要介绍了SpringBoot中的拦截器(Interceptor)及其与过滤器(Filt... 目录前言一、核心功能二、拦截器的实现2.1 定义自定义拦截器2.2 注册拦截器三、多拦截器的执行顺序四、过

JAVA线程的周期及调度机制详解

《JAVA线程的周期及调度机制详解》Java线程的生命周期包括NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING和TERMINATED,线程调度依赖操作系统,采用抢占... 目录Java线程的生命周期线程状态转换示例代码JAVA线程调度机制优先级设置示例注意事项JAVA线程

JavaWeb项目创建、部署、连接数据库保姆级教程(tomcat)

《JavaWeb项目创建、部署、连接数据库保姆级教程(tomcat)》:本文主要介绍如何在IntelliJIDEA2020.1中创建和部署一个JavaWeb项目,包括创建项目、配置Tomcat服务... 目录简介:一、创建项目二、tomcat部署1、将tomcat解压在一个自己找得到路径2、在idea中添加

Java使用Spire.Doc for Java实现Word自动化插入图片

《Java使用Spire.DocforJava实现Word自动化插入图片》在日常工作中,Word文档是不可或缺的工具,而图片作为信息传达的重要载体,其在文档中的插入与布局显得尤为关键,下面我们就来... 目录1. Spire.Doc for Java库介绍与安装2. 使用特定的环绕方式插入图片3. 在指定位

springboot的controller中如何获取applicatim.yml的配置值

《springboot的controller中如何获取applicatim.yml的配置值》本文介绍了在SpringBoot的Controller中获取application.yml配置值的四种方式,... 目录1. 使用@Value注解(最常用)application.yml 配置Controller 中