深入JVM:详解JIT即时编译器
文章目录
- 深入JVM:详解JIT即时编译器
- 一、序言
- 二、基础概念
- 1、何为JIT即时编译
- 2、热点代码
- 三、HotSpot内置的即时编译器
- 1、C1编译器
- 2、C2编译器
- 3、分层编译
- 3.1 协作流程
- 四、常见JIT优化技术
- 1、方法内联
- 2、逃逸分析
- (1)同步锁消除
- (2)栈上分配
- (3)标量替换
- 五、后记
深入JVM:详解JIT即时编译器
一、序言
对于Java工程师而言,深入理解JVM(Java虚拟机)不仅是掌握Java程序运行机制的基础,也是提升系统性能、优化应用和解决复杂问题能力的重要一步,更是Java进阶之路的重中之重。
本文小豪将带大家认识JIT即时编译器,介绍主流HotSpot虚拟机内置的几款即时编译器,同时结合代码实例,着重讲解JIT优化手段,话不多说,我们直接进入正题。
二、基础概念
在之前,网上可能经常说Java语言的运行速度不如C或C++,Java运行速度慢主要是因为它是解释执行的,而C或C++是编译执行的,解释执行需要通过JVM虚拟机将字节码实时翻译成机器码(边翻译边执行),才能运行在操作系统上,这个过程会比编译执行慢。
但现在再说这个结论就不太对了,随着JIT即时编译技术的发展,性能差距正在逐步缩小,甚至在某些情况下,执行速度是优于C或C++的。
1、何为JIT即时编译
在Java程序执行过程中,主要依靠字节码指令来进行。这些字节码指令按照顺序逐行被实时翻译成机器码,以便于在操作系统上运行。当某些方法或代码块(它们都对应特定的字节码)被频繁调用时,这部分代码就被视为热点代码。
JVM虚拟机会针对性的对这部分热点代码进行优化编译,将它们从字节码转换为本地机器码,然后将优化后的本地机器码缓存起来,后续再执行时可以直接从缓存中获取并运行,无需再次编译。
完成这个过程的编译器,就称为JIT即时编译器(Just In Time Compiler)。JIT即时编译器显著提升了Java的性能,缩小了与其它编译型语言运行速度的差距。
2、热点代码
热点代码是指在运行过程中被频繁执行的代码,被即时编译的热点代码有两种,分别是:
- 被多次调用的方法
- 被多次执行的循环体
而热点代码由热点探测进行发现,热点探测基于计数器,JVM虚拟机会为每个方法建立对应的计数器,统计方法的执行次数、方法内的循环次数等,如果计数器超过指定阈值,则标识其为热点代码。
方法调用计数器:统计方法被调用的次数
回边计数器:统计方法内循环体代码执行的次数
三、HotSpot内置的即时编译器
主流的HotSpot虚拟机内置了两个JIT编译器:C1(Client Compiler)编译器和C2(Server Compiler)编译器,在常用的JDK 8版本中,C1和C2是相互协同工作的。
1、C1编译器
C1编译器注重启动时间和编译时间,编译速度较快,但优化程度相对较低。
C1的优化策略相对简单,也比较轻量级,比如方法内联、公共子表达式消除,C1编译器编译的代码的执行速度通常比C2编译器慢。
2、C2编译器
C2编译器侧重于深度优化,与C1正好相反,C2编译器的编译时间较长,但优化的程度较高。
C2的优化策略比较深度,会进行更高级的优化,比如逃逸分析等,C2编译器编译的代码的执行速度通常比C1编译器快。
C2编译器由于深度优化代码过于复杂,已经很难维护了,从JDK 10开始,Graal编译器已经代替了C2编译器,与C1编译器协同工作
3、分层编译
由于C1和C2编译器在优化方面有不同的侧重点:C1侧重编译速度,C2侧重深度优化。
从JDK 7开始,采用分层编译的方式,C1和C2相互协同,共同发挥作用,HotSpot虚拟机根据代码的运行性能动态的选择具体采用哪个编译器完成优化。
在分层编译中,整个优化过程分为五个层次,对应着不同的优化策略:
- 第0层:解释器执行,开启
Profiling
性能监控功能 - 第1层:C1编译器执行,不开启
Profiling
,执行不带Profiling
功能的C1编译代码 - 第2层:C1编译器执行,开启
Profiling
,仅执行带部分Profiling
功能的C1编译代码(方法调用次数和循环次数) - 第3层:C1编译器执行,开启
Profiling
,执行带全部Profiling
功能的C1编译代码 - 第4层:C2编译器执行,执行C2编译代码
Profiling
性能监控是指在程序执行过程中,收集程序执行的各项数据,包括方法调用次数、循环次数、类型转换等等,收集的数据越多,额外的性能开销越大。第1层到第3层都是由C1编译器执行的,显然,第1层由于不开启
Profiling
,执行性能会高于第2层,同时第2层只收集少量数据,第2层的执行性能也会高于第3层(总体性能:第4层 > 第1层 > 第2层 > 第3层 > 第0层)
3.1 协作流程
在分层编译中,C1和C2编译器是相互协作的,它们都由各自独立的线程来处理编译任务,线程内部维护着任务队列,用来存放待编译的热点代码,具体协作流程如下:
- 默认情况下,热点方法首先会被第3层的C1编译器执行,当
Profiling
性能监控收集到的数值达到阈值,JVM会评估C1和C2的优化性能(字节码数较少等)- 若判定C1和C2的执行效率相当:交由第1层的C1编译器进行优化,停止
Profiling
监控,不再收集运行信息 - 若判定C2执行效率相当优于C1:交由第4层的C2编译器进行深度优化
- 若判定C1和C2的执行效率相当:交由第1层的C1编译器进行优化,停止
- C1线程忙碌时,会直接交由第4层的C2编译器进行深度优化
- C2线程忙碌时,会先交由第2层的C1编译器执行优化,进行信息的初步收集,待C2线程空闲时,再交由第3层的C1编译器执行
四、常见JIT优化技术
JIT即使编译器主要是通过方法内联和逃逸分析两种技术来实现代码的优化。
1、方法内联
方法内联其实就是将被调用方法的字节码直接插入到调用该方法的地方,调用时减少了栈帧的创建销毁开销。
举个例子:
实际上JIT修改的是字节码指令,为便于理解,本文使用代码举例
// 方法内联
public double methodInlining() {double num1 = Math.random();double num2 = Math.random();// 等价于 -> double sum = num1 + num2double sum = add(num1, num2);return sum;
}public double add(double num1, double num2) {return num1 + num1;
}
在代码中,方法内联会将其中的add(num1, num2)
方法转换为实际的num1 + num1
,直接进行计算操作,避免了方法调用。
这里我们简单测试一下:
public static void main(String[] args) {long startTime = System.currentTimeMillis();JitTest demo = new JitTest();for (int i = 0; i < 1000000; i++) {demo.methodInlining();}long endTime = System.currentTimeMillis();System.out.println("执行耗时:" + (endTime - startTime));
}
测试代码也比较简单,循环调用百万次,统计总耗时。
首先我们测试不启用JIT优化下这段代码的执行耗时,禁用JIT优化只需要添加JVM参数-Xint
即可,禁用后,控制台输出:
执行耗时:881
取消掉禁用参数-Xint
,启用JIT优化,重新执行这段测试代码:
执行耗时:47
我们进一步验证一下add
方法是否进行了方法内联优化,添加三个JVM参数-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
,打印JVM优化内容:
- -XX:+PrintCompilation:输出被JIT编译的方法信息
- -XX:+UnlockDiagnosticVMOptions:解锁用于诊断JVM的选项,默认关闭
- -XX:+PrintInlining:打印内联方法
如图,大量方法都被内联,其中也包括我们自己写的methodInlining
和add
方法,并且同时打印出了方法对应的字节大小,我们发现这些被内联的方法字节数都比较小。
其实这是由于方法内联也有一点的限制,即字节数过大的方法体不会进行内联,具体限制如下:
-
非热点代码:默认限制字节数小于
35
,可通过参数调整限制// 非热点代码的内联字节大小最大阈值 -XX:MaxInlineSize=n
-
热点代码:默认限制字节数小于
325
,可通过参数调整限制// 热点代码的内联字节大小最大阈值 -XX:FreqInlineSize=n
-
接口实现限制:若接口的实现方法数量超过3个,则不会被内联
2、逃逸分析
逃逸分析核心思想是判断方法内创建的对象是否会被外部所引用,当JIT编译器判断到对象在方法外部不会被引用,即该对象的生命周期被限定在当前方法执行期间,编译器就能采取多种优化措施,比如锁消除、标量替换和栈上分配。
(1)同步锁消除
同步锁消除旨在消除代码中的同步操作,若某个对象不会逃逸出当前方法,只在方法内部使用,则不存在线程安全问题,因为每个方法的栈都是私有的,JIT编译器就会消除该同步锁操作,提高程序的执行效率。
举个例子:
public void syncLockEliminate() {// 等价于 -> 不执行该同步锁synchronized (JitTest.class) {// 业务代码逻辑}
}public static void main(String[] args) {long startTime = System.currentTimeMillis();JitTest demo = new JitTest();for (int i = 0; i < 1000000; i++) {demo.syncLockEliminate();}long endTime = System.currentTimeMillis();System.out.println("执行耗时:" + (endTime - startTime));
}
这里启用和禁用JIT锁消除(禁用锁消除参数:-XX:-EliminateLocks
)对应控制台输出的执行耗时分别为:
// 启用锁消除
执行耗时:143// 禁用锁消除
执行耗时:203
(2)栈上分配
栈上分配是将被创建的对象内存分配在栈上,而不是堆上,降低GC垃圾回收的频率。
举个例子:
static class UserInfo {// 姓名private String name;// 年龄private int age;public UserInfo() {}public UserInfo(String name, int age) {this.name = name;this.age = age;}
}// 栈上分配,不赋值对象属性
public static void createUser() {UserInfo user = new UserInfo();
}public static void main(String[] args) throws Exception {long startTime = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {createUser();}long endTime = System.currentTimeMillis();System.out.println("执行耗时:" + (endTime - startTime));
}
这里我们验证一下这个结论,我们在JVM参数上添加-XX:+PrintGC
,输出GC日志信息,同时将堆内存空间设小一些:
-Xms10m -Xmn10m -XX:+PrintGC
首先默认启用JIT逃逸分析优化,控制台输出:
执行耗时:72
接着禁用逃逸分析优化,添加参数-XX:-DoEscapeAnalysis
关闭逃逸分析,控制台输出:
[GC (Allocation Failure) 7679K->1041K(9216K), 0.0010975 secs]
[GC (Allocation Failure) 8721K->904K(9216K), 0.0006550 secs]
[GC (Allocation Failure) 8584K->840K(9216K), 0.0007822 secs]
执行耗时:103
关闭逃逸分析优化后,JVM执行了几次的GC垃圾回收。
(3)标量替换
标量替换是将需要被创建的对象拆解成独立的标量(对象内部的基本数据类型),方法执行时不创建该对象实例,而是直接创建它的成员变量代替它,这些成员变量作为该方法的局部变量被分配在栈上。
标量替换是针对栈上分配更进一步的优化技术,栈上分配只是将对象从堆上分配到栈上,而标量替换是将对象进行拆解,作为方法的局部变量被分配在栈上。
举个例子:
// 标量替换,赋值对象属性
public static void createUser() {// 等价于 -> String name = user.name = xiaohao;// int age = user.name = 24UserInfo user = new UserInfo("xiaohao", 24);String userInfo = user.name + ":" + user.age;
}public static void main(String[] args) throws Exception {long startTime = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {createUser();}long endTime = System.currentTimeMillis();System.out.println("执行耗时:" + (endTime - startTime));
}
若JIT判断到UserInfo
对象不会逃逸出当前方法,则会将UserInfo
对象的name
和age
属性替换为两个局部变量,不进行创建UserInfo
对象,避免在堆上分配对象实例。
控制台输出的执行耗时分别为:
// 启用逃逸分析
执行耗时:162// 禁用逃逸分析
执行耗时:203
五、后记
本文从JIT即时编译器基础概念开始介绍,着重讲解了常见的JIT优化技术,包括方法内联与逃逸分析,额外拓展了HotSpot虚拟机内置的C1、C2编译器及分层编译协作流程。
JIT即时编译器在运行时对Java字节码进行优化,极大地提升了Java程序的执行效率。不过也要求我们日常编写代码要注意一定的规范,让JIT发挥最大效果。比如控制方法的代码行数,避免写过长的代码方法,可以将复杂的逻辑拆分成多个小方法,提升方法内联的生效几率;另外也可以控制接口的实现数量,尽量不超过两个,降低方法内联的复杂度;以及注意在方法内创建对象时,尽量不让对象逃逸。
如果大家觉得内容有价值,不妨考虑点点赞,关注关注小豪,后续小豪将会继续更新其它Java相关文章,大家共同进步~
相关文章:

深入JVM:详解JIT即时编译器
文章目录 深入JVM:详解JIT即时编译器一、序言二、基础概念1、何为JIT即时编译2、热点代码 三、HotSpot内置的即时编译器1、C1编译器2、C2编译器3、分层编译3.1 协作流程 四、常见JIT优化技术1、方法内联2、逃逸分析(1)同步锁消除(…...

ORBSLAM3_ROS_Ubuntu18_04环境搭建安装
orbslam3安装 ORB-SLAM3配置及安装教程(2023.3)_orbslam3安装-CSDN博客 换源,换成国内的 搜索software 安装工具 sudo apt install git sudo apt update sudo apt install gcc g cmake安装 cmake安装新版本 ubuntu20.04安装cmake详细…...

【opencv - C++ - Ubuntu】putText 显示中文最快方法
话不多说,直接上代码 #include <iostream> #include <opencv2/opencv.hpp> #include <opencv2/freetype.hpp>using namespace std; using namespace cv;int main(void) {Mat image(1000, 1800, CV_8UC3, Scalar(200,162,33));Ptr<freetype::F…...

百度网盘下载速度慢的解决办法
目录 一、背景 二、解决办法 1、点击三个竖点,再点设置 2、点击传输,再点击去开启该功能 3、点击同意,开启优化速率 三、结果 四、备注 一、背景 当你不是百度网盘会员时,你在使用百度网盘下载时,是否下载速度太…...

Python api接口 异步
Python API接口异步编程简介 在现代的软件开发中,大多数应用都需要通过API接口与其他系统进行交互。Python是一种非常流行的编程语言,因此许多开发者选择使用Python来构建他们的API接口。在一些情况下,API接口需要进行异步编程,以…...

Java 和 Kotlin 单例模式写法对比
目录 1、饿汉模式 Java 写法: Kotlin 写法: Kotlin 这段代码反编译&简化后如下: 2、懒汉模式,静态同步方法 Java 写法: Kotlin 写法: Kotlin 这段代码反编译&简化后如下: 3、懒…...

解析connectionReset异常的原因与解决方案
解析connectionReset异常的原因与解决方案 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将深入探讨Java中connectionReset异常的原因及其解决方案。这…...

mindspore打卡第9天 transformer的encoder和decoder部分
mindspore打卡第9天 transformer的encoder和decoder部分 import mindspore from mindspore import nn from mindspore import ops from mindspore import Tensor from mindspore import dtype as mstypeclass ScaledDotProductAttention(nn.Cell):def __init__(self, dropout_…...

Python实现IPv4地址和16进制互相转换
Python实现IPv4地址和16进制互相转换 import socketdef ip_to_hex16(ipaddr):# 使用 socket 库中的方法将IP地址转换为网络字节序的二进制表示hex_bytes socket.inet_aton(ipaddr)# 将二进制数据转换为整数, 其中byteorderbig 表示使用大端字节序(从高位到低位&…...

计算机视觉 | 基于图像处理和边缘检测算法的黄豆计数实验
目录 一、实验原理二、实验步骤1. 图像读取与预处理2. 边缘检测3. 轮廓检测4. 标记轮廓序号 三、实验结果四、完整代码 Hi,大家好,我是半亩花海。 本实验旨在利用 Python 和 OpenCV 库,通过图像处理和边缘检测算法实现黄豆图像的自动识别和计…...

深入分析 Android BroadcastReceiver (七)
文章目录 深入分析 Android BroadcastReceiver (七)1. 高级应用场景1.1 示例:动态权限请求1.2 示例:应用内通知更新 2. 安全性与性能优化2.1 示例:设置权限防止广播攻击2.2 示例:使用 LocalBroadcastManager2.3 示例:在…...

C++中的数据结构
一.STL标准库 结构:STL中有六大组件,分别是:容器,算法,迭代器,仿函数,配接器,配置器;以下分别介绍这六大组件中的最主要的三个。 1.容器 容器来配置存储空间,算法通过…...

武汉星起航:一站式服务,助力亚马逊卖家高效运营,实现收益飞跃
在跨境电商的浪潮中,武汉星起航电子商务有限公司以其独特的一站式跨境电商服务,为众多亚马逊卖家提供了强有力的支持,助力他们在不断发展的市场中脱颖而出,实现收益的大幅提升。 武汉星起航的一站式跨境电商服务,以其…...

从灵感到实践:Kimi辅助完成学术论文选题的文艺之旅
学境思源,一键生成论文初稿: AcademicIdeas - 学境思源AI论文写作 昨天我们为大家介绍了ChatGPT辅助完成实现设计(AI与学术的交响:ChatGPT辅助下的实验设计新篇章)。今天我们再来看看Kimi对于论文选题都能提供哪些帮助…...

华为od-C卷200分题目4 -电脑病毒感染
华为od-C卷200分题目4 -电脑病毒感染 一个局域网内有很多台电脑,分别标注为0 - N-1的数字。相连接的电脑距离不一样,所以感染时间不一样,感染时间用t表示。其中网络内一个电脑被病毒感染,其感染网络内所有的电脑需要最少需要多长…...

show-overflow-tooltip 解决elementui el-table标签自动换行的问题
elementui中 el-table中某一行的高度不想因为宽度不够而撑开换行展示的解决方法。可通过show-overflow-tooltip属性解决,如下 代码是这样的 <el-table-column width"80" prop"id" label"ID"></el-table-column> <el…...

数字社交的领航者:解析Facebook的引领作用
在当今数字化社会中,社交网络已经成为了人们日常生活不可或缺的一部分。而在众多社交平台中,Facebook凭借其巨大的用户基础和创新的技术应用,被公认为数字社交领域的领航者之一。本文将深入解析Facebook在数字社交中的引领作用,探…...

深度分析 Apache Flink 窗口机制
什么是 Flink 窗口? Apache Flink 是一个用于处理实时流数据的开源框架,其核心功能之一是窗口(Window)机制。窗口是 Flink 在处理流数据时用于划分数据流的逻辑概念,它将无限的流数据切割成有限的、可管理的部分&…...

ubuntu 软链接(ubuntu20.04)
ubuntu 软链接(ubuntu20.04) 在Ubuntu和其他Linux系统中,软链接(也称为符号链接)是文件系统中的一个特殊类型的文件,它作为一个引用或指针,指向另一个文件或目录。软链接类似于Windows中的快捷…...

如何在LabVIEW中使用FPGA模块
LabVIEW FPGA模块是NI公司推出的一款强大工具,它允许用户使用LabVIEW图形化编程环境来开发FPGA(现场可编程门阵列)应用程序。与传统的HDL(硬件描述语言)编程相比,LabVIEW FPGA模块大大简化了FPGA开发的过程…...

FPGA开发技能(7)Vivado设置bit文件加密
文章目录 前言1. AES加密原理2.xilinx的AES方案3.加密流程3.1生成加密的bit流3.2将密钥写入eFUSE寄存器 4.验证结论5.传送门 前言 在FPGA的项目发布的时候需要考虑项目工程加密的问题,一方面防止自己的心血被盗,另一方面也保护公司资产,保护知…...

【算法专题--链表】旋转链表 -- 高频面试题(图文详解,小白一看就懂!!)
目录 一、前言 二、题目描述 三、解题方法 ⭐解题思路---闭合为环 🍍 案例图解 四、总结与提炼 五、共勉 一、前言 旋转链表 这道题,可以说是--链表专题--,最经典的一道题,也是在面试中频率最高的一道题目&#x…...

ElasticSearch 和 MySQL的区别
MySQLElasticSearch 数据库(database)索引(index)数据表(table) 类型(type) 记录文档(document,json格式) 一、ES基础命令 1. ES cat查询命令 2.…...

Linux部署wordpress站点
先安装宝塔面板 yum install -y wget && wget -O install.sh https://download.bt.cn/install/install_6.0.sh && sh install.sh ed8484bec 因为wordpress需要php,mysql,apache ,httpd环境 参考:Linux 安装宝塔…...

实体零售连锁企业如何通过物流接口实现数智化转型升级?
在电子商务浪潮的持续冲击下,传统的实体零售行业面临着巨大的挑战。为了在线上线下融合的新零售时代保持竞争力,众多实体零售企业积极寻求数字化转型的突破。 某中国零售连锁百强企业近年来致力于打造自有品牌的线上销售体系,自2021年8月起接…...

AWS EKS上GPU工作负载自动扩缩容的异常排查指南
在AWS EKS上使用Karpenter和KEDA实现GPU工作负载的自动扩缩容是一个复杂的过程,涉及多个组件的协同工作。当遇到问题时,系统性的排查方法可以帮助我们快速定位和解决问题。本文将详细介绍如何对这个系统进行全面的异常排查。 1. Karpenter相关组件检查 1.1 NodePool检查 N…...

Pytest+Allure+Yaml+Jenkins+Gitlab接口自动化中Jenkins配置
一、背景 Jenkins(本地宿主机搭建) 拉取GitLab(服务器)代码到在Jenkins工作空间本地运行并生成Allure测试报告 二、框架改动点 框架主运行程序需要先注释掉运行代码(可不改,如果运行报allure找不到就直接注释掉) …...

应用及安全
目录 一、PAM 安全认证及配置 1.1配置 su 命令的认证 1.2PAM 配置文件结构二、账号和密码安全管理 2.1账号管理 2.2系统账号清理 2.3密码安全控制 2.4密码重设示例 2.5参考命令三、命令历史限制 3.1设置命令历史记录…...

字节流和字符流的相关知识
目录 1. Writer1.1 写两行数据1.2 换一种方式1.3 追加数据1.4 写很多数据,记得要清一下缓存1.5 用数组、字符串写入 2. Reader2.1 读个文件2.2 读取字符2.3 读取数据到数组2.4 复制文件 3. InputStream4. OutputStream5. 参考链接 1. Writer Writer类是Java.io包中…...

LLM意图识别器实践
利用 Ollama 和 LangChain 强化条件判断语句的智能提示分类 ❝ 本文译自Supercharging If-Statements With Prompt Classification Using Ollama and LangChain一文,以Lumos工具为例,讲解了博主在工程实践中,如何基于LangChain框架和本地LLM优…...