即时编译器在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

相关文章

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

Java中的.close()举例详解

《Java中的.close()举例详解》.close()方法只适用于通过window.open()打开的弹出窗口,对于浏览器的主窗口,如果没有得到用户允许是不能关闭的,:本文主要介绍Java中的.... 目录当你遇到以下三种情况时,一定要记得使用 .close():用法作用举例如何判断代码中的 input

前端缓存策略的自解方案全解析

《前端缓存策略的自解方案全解析》缓存从来都是前端的一个痛点,很多前端搞不清楚缓存到底是何物,:本文主要介绍前端缓存的自解方案,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、为什么“清缓存”成了技术圈的梗二、先给缓存“把个脉”:浏览器到底缓存了谁?三、设计思路:把“发版”做成“自愈”四、代码

Spring Gateway动态路由实现方案

《SpringGateway动态路由实现方案》本文主要介绍了SpringGateway动态路由实现方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随... 目录前沿何为路由RouteDefinitionRouteLocator工作流程动态路由实现尾巴前沿S

JavaScript对象转数组的三种方法实现

《JavaScript对象转数组的三种方法实现》本文介绍了在JavaScript中将对象转换为数组的三种实用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友... 目录方法1:使用Object.keys()和Array.map()方法2:使用Object.entr

idea+spring boot创建项目的搭建全过程

《idea+springboot创建项目的搭建全过程》SpringBoot是Spring社区发布的一个开源项目,旨在帮助开发者快速并且更简单的构建项目,:本文主要介绍idea+springb... 目录一.idea四种搭建方式1.Javaidea命名规范2JavaWebTomcat的安装一.明确tomcat