JIT即时编译器深度解析——Java性能提升利器

news2024/12/26 11:13:07

文章目录

  • 一、JIT概述
    • 1、为什么要用JIT即时编译器
    • 2、C1、C2与Graal编译器
    • 3、分层编译
    • 4、热点代码
    • 5、热点探测
      • (1)方法调用计数器
      • (2)回边计数器
  • 二、编译优化技术
    • 1、方法内联
      • (1)什么是方法内联
      • (2)代码演示
      • (3)总结
    • 2、锁消除
    • 3、锁粗化
    • 4、标量替换
      • (1)什么是标量替换
      • (2)逃逸分析技术
      • (3)代码演示
      • (4)原因分析

一、JIT概述

1、为什么要用JIT即时编译器

Java程序在运行的时候,主要就是执行字节码指令,一般这些指令会按照顺序解释执行,这种就是解释执行。

在这里插入图片描述

但是那些被频繁调用的代码,比如调用次数很高或者在 for 循环里的那些代码,如果按照解释执行,效率是非常低的。(这个就是Java以前被C、C++开发者吐槽慢的原因)

以上的这些代码称为热点代码。所以,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。

完成这个任务的编译器,就称为即时编译器(Just In Time Compiler),简称 JIT 编译器。

在这里插入图片描述

2、C1、C2与Graal编译器

在JDK1.8中 HotSpot 虚拟机中,内置了两个 JIT,分别为 C1 编译器和 C2 编译器。
在这里插入图片描述
C1编译器
C1 编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序,例如,GUI 应用对界面启动速度就有一定要求,C1也被称为 Client Compiler。
C1编译器几乎不会对代码进行优化。

C2编译器
C2 编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序。根据各自的适配性,这种即时编译也被称为Server Compiler。
但是C2代码已超级复杂,无人能维护!所以才会开发Java编写的Graal编译器取代C2(JDK10开始)

3、分层编译

在 Java7之前,需要根据程序的特性来选择对应的 JIT,虚拟机默认采用解释器和其中一个编译器配合工作。

Java7及以后引入了分层编译,这种方式综合了 C1 的启动性能优势和 C2 的峰值性能优势,当然我们也可以通过参数强制指定虚拟机的即时编译模式。

Java8 中,默认开启分层编译

通过 java -version 命令行可以直接查看到当前系统使用的编译模式(默认分层编译):
在这里插入图片描述
使用“-Xint”参数强制虚拟机运行于只有解释器的编译模式:
在这里插入图片描述
使用“-Xcomp”强制虚拟机运行于只有 JIT 的编译模式下:
在这里插入图片描述
JVM 的执行状态分为了 5 个层次:
Ø 第 0 层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第二层编译;
Ø 第 1 层:可称为 C1 编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling;
Ø 第 2 层:也称为 C1 编译,开启Profiling,仅执行带方法调用次数和循环回边执行次数 profiling 的 C1 编译;
Ø 第 3 层:也称为 C1 编译,执行所有带 Profiling 的 C1 编译;
Ø 第 4 层:可称为 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

4、热点代码

热点代码,就是那些被频繁调用的代码,比如调用次数很高或者在 for 循环里的那些代码。这些再次编译后的机器码会被缓存起来,以备下次使用,但对于那些执行次数很少的代码来说,这种编译动作就纯属浪费。

JVM提供了一个参数“-XX:ReservedCodeCacheSize”,用来限制 CodeCache 的大小。也就是说,JIT 编译后的代码都会放在 CodeCache 里。

如果这个空间不足,JIT 就无法继续编译,编译执行会变成解释执行,性能会降低一个数量级。同时,JIT 编译器会一直尝试去优化代码,从而造成了 CPU 占用上升。

通过 java -XX:+PrintFlagsFinal –version查询:
在这里插入图片描述

5、热点探测

在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。

虚拟机为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。

(1)方法调用计数器

用于统计方法被调用的次数,方法调用计数器的默认阈值在客户端模式下是 1500 次,在服务端模式下是 10000 次(我们用的都是服务端,java –version查询),可通过 -XX: CompileThreshold 来设定
在这里插入图片描述
通过 java -XX:+PrintFlagsFinal –version查询
在这里插入图片描述

(2)回边计数器

用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge),该值用于计算是否触发 C1 编译的阈值,在不开启分层编译的情况下,在服务端模式下是10700。

怎么算的呢!参考以下公式:

回边计数器阈值 =方法调用计数器阈值(CompileThreshold)×(OSR比率(OnStackReplacePercentage)-解释器监控比率(InterpreterProfilePercentage)/100

通过 java -XX:+PrintFlagsFinal –version查询相关参数:
在这里插入图片描述
在这里插入图片描述
其中OnStackReplacePercentage默认值为140,InterpreterProfilePercentage默认值为33,如果都取默认值,那Server模式虚拟机回边计数器的阈值为10700.

回边计数器阈值 =10000×(140-33)=10700

二、编译优化技术

JIT 编译运用了一些经典的编译优化技术来实现代码的优化,即通过一些例行检查优化,可以智能地编译出运行时的最优性能代码。

1、方法内联

(1)什么是方法内联

方法内联的优化行为就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。

例如以下方法:
在这里插入图片描述
最终会被优化为:
在这里插入图片描述
JVM 会自动识别热点方法,并对它们使用方法内联进行优化。

我们可以通过 -XX:CompileThreshold 来设置热点方法的阈值。

但要强调一点,热点方法不一定会被 JVM 做内联优化,如果这个方法体太大了,JVM 将不执行内联操作。

而方法体的大小阈值,我们也可以通过参数设置来优化:

经常执行的方法,默认情况下,方法体大小小于 325 字节的都会进行内联,我们可以通过 -XX:FreqInlineSize=N 来设置大小值;
在这里插入图片描述
不是经常执行的方法,默认情况下,方法大小小于 35 字节才会进行内联,我们也可以通过 -XX:MaxInlineSize=N 来重置大小值。
在这里插入图片描述

(2)代码演示

/**
 * 方法内联
 * -XX:+PrintCompilation   //在控制台打印编译过程信息
 * -XX:+UnlockDiagnosticVMOptions //解锁对JVM进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对JVM进行诊断
 * -XX:+PrintInlining //将内联方法打印出来
 */
public class CompDemo {
    private int add1(int x1, int x2, int x3, int x4) {
        return add2(x1, x2) + add2(x3, x4);//
    }

    private int add2(int x1, int x2) {
        return x1 + x2;
    }

    //内联后的调用类似于以下方法
    private int add(int x1, int x2, int x3, int x4) {
        return x1 + x2 + x3 + x4;
    }

    public static void main(String[] args) {
        CompDemo compDemo = new CompDemo();
        //方法调用计数器的默认阈值10000次,我们循环遍历超过需要阈值
        for (int i = 0; i < 1000000; i++) {
            compDemo.add1(1, 2, 3, 4);
        }

    }
}

设置 VM 参数:-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
-XX:+PrintCompilation //在控制台打印编译过程信息
-XX:+UnlockDiagnosticVMOptions //解锁对JVM进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对JVM进行诊断
-XX:+PrintInlining //将内联方法打印出来

在这里插入图片描述
我们发现,热点代码触发了内联。

但是如果循环次数太少的话,是不会触发内联的:
在这里插入图片描述
在这里插入图片描述

(3)总结

热点方法的优化可以有效提高系统性能,一般我们可以通过以下几种方式来提高方法内联:

通过设置 JVM 参数来减小热点阈值或增加方法体阈值,以便更多的方法可以进行内联,但这种方法意味着需要占用更多地内存;

在编程中,避免在一个方法中写大量代码,习惯使用小方法体

尽量使用 final、private、static 关键字修饰方法,编码方法因为继承,会需要额外的类型检查。

2、锁消除

在非线程安全的情况下,尽量不要使用线程安全容器,比如 StringBuffer。由于 StringBuffer 中的 append 方法被 Synchronized 关键字修饰,会使用到锁,从而导致性能下降。
在这里插入图片描述
但实际上,在以下代码测试中,StringBuffer 和 StringBuilder 的性能基本没什么区别。这是因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。

下代码测试中,StringBuffer 和 StringBuilder 的性能基本没什么区别。这是因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。
在这里插入图片描述
在这里插入图片描述

我们把锁消除关闭—测试发现性能差别有点大
-XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除
在这里插入图片描述

3、锁粗化

这段代码:
在这里插入图片描述
这里我们不考虑编译器优化的情况,如果在 for 循环中定义锁,那么锁的范围很小,但每次 for 循环都需要进行加锁和释放锁的操作,性能是很低的;但如果我们直接在 for 循环的外层加一把锁,那么对于同一个对象操作这段代码的性能就会提高很多,如下伪代码所示:

在这里插入图片描述
锁粗化的作用:如果检测到同一个对象执行了连续的加锁和解锁的操作,则会将这一系列操作合并成一个更大的锁,从而提升程序的执行效率。

4、标量替换

(1)什么是标量替换

逃逸分析证明一个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执行的时候可能不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了。这种编译优化就叫做标量替换(前提是需要开启逃逸分析)。

-XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启)
-XX:-DoEscapeAnalysis 关闭逃逸分析

-XX:+EliminateAllocations开启标量替换(jdk1.8默认开启)
-XX:-EliminateAllocations 关闭标量替换

(2)逃逸分析技术

在这里插入图片描述
逃逸分析的原理:分析对象动态作用域,当一个对象在方法中定义后,它可能被外部方法所引用。

比如:调用参数传递到其他方法中,这种称之为方法逃逸。甚至还有可能被外部线程访问到,例如:赋值给其他线程中访问的变量,这个称之为线程逃逸。

从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度。

如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高JVM的效率。

当然逃逸分析技术属于JIT的优化技术,所以必须要符合热点代码,JIT才会优化,另外对象如果要分配到栈上,需要将对象拆分,这种编译优化就叫做标量替换技术。

如下图中foo方法如果使用标量替换的话,那么最后执行的话就是foo1方法的效果。

/**
 * 标量替换
 *
 * -XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启)
 * -XX:-DoEscapeAnalysis 关闭逃逸分析
 *
 * -XX:+EliminateAllocations开启标量替换(jdk1.8默认开启)
 * -XX:-EliminateAllocations 关闭标量替换
 */
public class VariableDemo {

	// 原代码:创建了一个Teacher
    public void foo() {
        Teacher teacher = new Teacher();
        teacher.name = "zhangsan";
        teacher.age = 18;
        //to do something
    }

	// 如果没有发生逃逸,就将该对象直接拆解为对应的属性
    public void foo1() {
        String name = "zhangsan";
        int age = 18;
        //to do something
    }

}

class Teacher{
    String name;
    String sexType;
    int age;

	// get set
}

(3)代码演示

/**
 * 逃逸分析-栈上分配
 * -XX:-DoEscapeAnalysis  -XX:-EliminateAllocations -XX:+PrintGC
 */
public class EscapeAnalysisTest {
    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 50000000; i++) {//5000万次---5000万个对象
            allocate();
        }
        System.out.println((System.currentTimeMillis() - start) + " ms");
        Thread.sleep(600000);
    }

    static void allocate() {//逃逸分析(不会逃逸出方法)
        //这个myObject引用没有出去,也没有其他方法使用
        MyObject myObject = new MyObject(2020, 2020.6);
    }

    static class MyObject {
        int a;
        double b;

        MyObject(int a, double b) {
            this.a = a;
            this.b = b;
        }
    }
}

这段代码在调用的过程中Myboject这个对象属于不可逃逸,JVM可以做栈上分配,所以运行速度非常快!
JVM默认会做逃逸分析、会进行标量替换,会进行栈上分配。
在这里插入图片描述
然后关闭逃逸分析:-XX:-DoEscapeAnalysis
在这里插入图片描述

测试结果可见,开启逃逸分析对代码的执行性能有很大的影响!

(4)原因分析

如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。

采用了逃逸分析后,满足逃逸的对象在栈上分配

没有开启逃逸分析,对象都在堆上分配,会频繁触发垃圾回收(垃圾回收会影响系统性能),导致代码运行慢

开启GC打印日志:-XX:+PrintGC

开启逃逸分析:
在这里插入图片描述
可以看到没有GC日志(因为进行了栈上分配)。

关闭逃逸分析:
在这里插入图片描述
可以看到关闭了逃逸分析,JVM在频繁的进行垃圾回收(GC),正是这一块的操作导致性能有较大的差别。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1306941.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

银行数据分析指标篇:最全银行数据指标体系打包送给你!

前两天分享了银行业数据分析的案例&#xff0c;今天呢&#xff0c;老李把金融行业的指标体系和典型分析场景完整分享给大家&#xff01;做地通俗易懂&#xff0c;条理清晰&#xff0c;很快就能上手。 银行指标体系 “指标”作为业务和数据的结合&#xff0c;它使得业务目标可…

Vue 2.0源码分析-update

Vue 的 _update 是实例的一个私有方法&#xff0c;它被调用的时机有 2 个&#xff0c;一个是首次渲染&#xff0c;一个是数据更新的时候&#xff1b;由于我们这一章节只分析首次渲染部分&#xff0c;数据更新部分会在之后分析响应式原理的时候涉及。_update 方法的作用是把 VNo…

聚焦本田XR-V和福特领睿:两大SUV综合实力对比,谁更胜一筹?

在当今的SUV市场中&#xff0c;家庭用户的选择变得越来越多样化。特别是对于那些追求时尚、功能性以及技术先进性的用户来说&#xff0c;选择正确的SUV显得尤为重要。本文将重点对比福特领睿和本田XR-V这两款SUV&#xff0c;探讨它们在各方面的表现&#xff0c;做一个综合实力的…

艾江山:你好好养生,我带你去看海

有多少人&#xff0c;还没好好看过这个世界&#xff1b; 有多少人&#xff0c;因为不够健康缺少走出来的勇气。 艾江山第二届平凡人的养生故事大赛暨北海游学&#xff0c;在一片依依不舍中圆满闭幕。 游学博闻&#xff0c;盖谓其因游学所以能博闻也 传统上&#xff0c;游学是…

Power BI - 5分钟学习增加条件列

每天5分钟&#xff0c;今天介绍Power BI增加条件列。 什么是增加条件列&#xff1f; 简单理解&#xff0c;可以根据表中某列设置一个或者多个条件&#xff0c;判定的结果会生成一个新列。 举例&#xff1a; 首先&#xff0c;导入一张【Sales】样例表(Excel数据源导入请参考每…

将自己的django项目部署到云服务器(腾讯云centos)

最近自己买了个云服务玩&#xff0c;突然就想把自己写的小项目部署到云服务器上&#xff0c;这样就可以实现公网访问了。以下是整个部署过程和遇到的各种问题的解决方案&#xff0c;有想自己部署自己功能的&#xff0c;可以参考着进行哦。 1、设置好腾讯云的远程登录代码 先给…

智能座舱架构与芯片- (9) 音频篇 上

一、音频总线 音频是智能座舱的核心功能&#xff0c;涵盖车载音响、语音识别、e-Call、消噪及回声消除等应用&#xff0c;随着汽车智能网联化的发展&#xff0c;对音频的开发要求也越来越高。传统的车载音频系统采用模拟并行音频信号传输方式&#xff0c;难以在功能增加与整车…

[足式机器人]Part2 Dr. CAN学习笔记-自动控制原理Ch1-1开环系统与闭环系统Open/Closed Loop System

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记-自动控制原理Ch1-1开环系统与闭环系统Open/Closed Loop System EG1: 烧水与控温水壶EG2: 蓄水与最终水位闭环控制系统 EG1: 烧水与控温水壶 EG2: 蓄水与最终水位 h ˙ q i n A − g h A R \dot{…

总结了人工智能领域,能源领域,电气领域比较好中的一些sci期刊!!仅供参考

文章目录 前言一、总结了人工智能领域&#xff0c;能源领域&#xff0c;电气领域比较好中的一些sci期刊 总结 前言 期刊查询网站&#xff1a; https://www.letpub.com.cn/index.php?pagejournalapp&viewsearch 链接: 点我跳转期刊查询网站 一、总结了人工智能领域&#…

许战海战略文库|美国品牌实践:从品类品牌向产业品牌转变

引言&#xff1a;《品类战略》是上世纪70年代特劳特和里斯所推崇的定位理论,强调“品类聚焦是唯一正确的战略“新品类要使用新品牌”等战略思想,并对品牌延伸等多元化品牌进行批判,并由中国代理人传入中国&#xff0c;从2002年至今滋生了众多品类品牌,阻碍中国经济发展。 在今天…

图生视频AI技术,1张图零提示词,让静态照片动起来

AI时代的发展速度比我们想象中的快多了&#xff0c;当大部分人刚学会AI生成图片时&#xff0c;现在又开始流行AI生成视频了&#xff0c;正式从图片、文字升级到短视频时代。 最近一段时间&#xff0c;AI生成视频的技术正在突飞猛进。Pika、Runway等大家熟知的海外工具都在不断…

android studio flutter启动一直卡在“ Running gradle task ‘assembledebug‘ “的解决问题

解决问题&#xff1a; 1、修改项目中android/build.gradle文件 将buildscript.repositories下面的 //google() //mavenCentral()注释掉&#xff0c;改成maven {allowInsecureProtocol trueurl https://maven.aliyun.com/repository/google } maven {allowInsecureProtocol t…

Chrome安装插件出现CRX-HEADER-INVALID解决方法

1 Chrome浏览器安装离线插件时出现了“CRX-HEADER-INVALID”错误。 2将插件包的后缀名改成.zip格式。 3点击右侧三点按钮后点击【更多工具】--》【扩展程序】界面。 4在【扩展程序】将ZIP包拉入并安装。 5这样就安装成功了&#xff0c;虽然图标上有红色图标…

HarmonyOS4.0从零开始的开发教程14Web组件的使用

HarmonyOS&#xff08;十二&#xff09;Web组件的使用 1 概述 相信大家都遇到过这样的场景&#xff0c;有时候我们点击应用的页面&#xff0c;会跳转到一个类似浏览器加载的页面&#xff0c;加载完成后&#xff0c;才显示这个页面的具体内容&#xff0c;这个加载和显示网页的…

数据库——安全性

智能2112杨阳 一、目的与要求&#xff1a; 1、设计用户子模式 2、根据实际需要创建用户角色及用户&#xff0c;并授权 3、针对不同级别的用户定义不同的视图&#xff0c;以保证系统的安全性 二、内容&#xff1a; 先创建四类用户角色&#xff1a; 管理员角色Cusm、客户角…

YashanDB亮相2023年深圳市高校教育信息化学会学术年会,加码高校数字基座建设

12月3日&#xff0c;“2023年深圳市高校教育信息化学会学术年会”在深圳顺利召开。深圳计算科学研究院&#xff08;简称&#xff1a;深算院&#xff09;YashanDB团队受邀出席&#xff0c;同来自深圳高校、政府教育部门、教育信息化专家学者等共同探讨教育信息化前沿技术与应用的…

06线性回归衍生算法

目录&#xff1a; ridge算法 lasso算法 elastic-Net算法 from sklearn.linear_model import Ridge from sklearn.linear_model import SGDRegressor, LinearRegression from sklearn.linear_model import Lasso import numpy as np# 1. Ridge 岭回归# 生成随机数据 X np.…

linux 调试工具 GDB 使用

gdb是linux下常用的代码调试工具&#xff0c;本文记录常用命令。 被调试的应用需要使用 -g 参数进行编译&#xff0c;如不确定可使用如下命令查看是否支持debug readelf -S filename | grep "debug" 启动调试 gdb binFile 例如要调试sshd&#xff1a; 调试带参数…

汉诺塔(函数递归)

前言 汉诺塔问题是一个经典的数学谜题&#xff0c;也是函数递归的一个经典问题&#xff0c;起源于印度。问题的设定是有三个柱子&#xff0c;第一个柱子上有一组不同大小的圆盘&#xff0c;按照从上到下依次变大的顺序摆放。目标是将所有的圆盘从第一个柱子移动到第三个柱子上&…

RV1126/RV1109 ISP调试方案

最近一直在做瑞芯微rv1126的开发&#xff0c;由于项目性质&#xff0c;与camera打的交道比较多&#xff0c;包括图像的采集&#xff0c;ISP处理&#xff0c;图像处理&#xff0c;H.264/H.265编解码等各个方面吧。学到了不少&#xff0c;在学习的过程中&#xff0c;也得到了不少…