关于JVM和OS中的指令重排以及JIT优化
关于JVM和OS中的指令重排以及JIT优化
前言:
这东西应该很重要才对,可是大多数博客都是以讹传讹,全是错误,尤其是JVM会对字节码进行重排都出来了,明明自己测一测就出来的东西,写出来误人子弟…
研究了两天,算是有点名堂了,只是不能看到到CPU的重排过程有点可惜
纸上得来终觉浅,建议手动截一下字节码以及汇编自己研究一下,肯定会有不一样的收获
关于JMM和JIT可以尝试看一下油管Jakob Jenkov的教程,很不错!
通俗易懂的说,指令重排是为了最大化执行效率,会在保证语意不变的情况下,调整代码的顺序。
而JIT会修改优化代码中的热点部分,使其效率大幅提升
OS中的指令重排:
比如:
a = b + c;
b = a + c;
d = e + f;
e = d + f;
这段代码可能会被调整为:
a = b + c;
d = e + f;
b = a + c;
e = d + f;
但是肯定不会调整为:
b = a + c;
a = b + c;
d = e + f;
e = d + f;
因为这样改变了代码语意
具体会调成什么样取决于JVM和OS
实际上来说,指令重排并不是以一行java代码为单位进行的,也就是说,我举的例子并不恰当
一行代码是由多句指令构成的,比如一个简单的Java程序:
public class test {public static void main(String[] args) {int a = 1;int b = 2;int c = a + b;}
}
其转化为字节码:
public class test {// 构造函数的声明public <init>()V // 这是无参构造方法,返回类型为 voidL0LINENUMBER 1 L0 // 表示该字节码位置对应源代码的第 1 行ALOAD 0 // 将当前对象(this)加载到栈上。这里的 0 表示加载 this(当前对象)。INVOKESPECIAL java/lang/Object.<init> ()V // 调用父类 Object 的构造方法(<init>),构造函数是无参的RETURN // 从构造方法中返回L1LOCALVARIABLE this Ltest; L0 L1 0 // 在字节码中定义了一个局部变量 'this',类型是 test,对应的范围是 L0 到 L1,局部变量索引为 0MAXSTACK = 1 // 最大栈深度为 1MAXLOCALS = 1 // 最大局部变量数为 1// main 方法的声明public static main([Ljava/lang/String;)V // main 方法,接受字符串数组作为参数,返回类型为 voidL0LINENUMBER 3 L0 // 表示该字节码位置对应源代码的第 3 行ICONST_1 // 将常量 1 压入栈中ISTORE 1 // 将栈顶的值(1)存入局部变量 1 中L1LINENUMBER 4 L1 // 表示该字节码位置对应源代码的第 4 行ICONST_2 // 将常量 2 压入栈中ISTORE 2 // 将栈顶的值(2)存入局部变量 2 中L2LINENUMBER 5 L2 // 表示该字节码位置对应源代码的第 5 行ILOAD 1 // 将局部变量 1 的值(即 1)加载到栈上ILOAD 2 // 将局部变量 2 的值(即 2)加载到栈上IADD // 将栈顶的两个整数相加(1 + 2 = 3)ISTORE 3 // 将结果(3)存入局部变量 3 中L3LINENUMBER 6 L3 // 表示该字节码位置对应源代码的第 6 行RETURN // 返回,从 main 方法中返回L4LOCALVARIABLE args [Ljava/lang/String; L0 L4 0 // 定义了局部变量 args,类型为 String[],范围是 L0 到 L4LOCALVARIABLE a I L1 L4 1 // 定义了局部变量 a,类型为 int,范围是 L1 到 L4LOCALVARIABLE b I L2 L4 2 // 定义了局部变量 b,类型为 int,范围是 L2 到 L4LOCALVARIABLE c I L3 L4 3 // 定义了局部变量 c,类型为 int,范围是 L3 到 L4MAXSTACK = 2 // 最大栈深度为 2MAXLOCALS = 4 // 最大局部变量数为 4
}
可以看到,转换成字节码多出了很多操作
其实字节码转成机器码/汇编时还会接着细分,为了演示就不再向下分析了
那么转换成字节码后我们会发现什么?
一个简单的 **int a = 1;**被转化成了
LINENUMBER 3 L0 // 表示该字节码位置对应源代码的第 3 行ICONST_1 // 将常量 1 压入栈中ISTORE 1 // 将栈顶的值(1)存入局部变量 1 中LOCALVARIABLE a I L1 L4 1 // 定义了局部变量 a,类型为 int,范围是 L1 到 L4
展示的目的在于,每一行代码把其溯源到底层的机器码,都是由一系列操作组成的
一般可以分为:
- 取指(IF):从存储器中读取指令,并将指令送入指令寄存器IR;同时更新程序计数器PC,指向下一条指令的地址。
- 译码(ID):对IR中的指令进行译码,确定操作码、操作数和功能;同时从寄存器文件中读取源操作数,并放入临时寄存器A和B中;如果有立即数,还要进行符号扩展,并放入临时寄存器Imm中。
- 执行(EX):根据操作码和功能,对A、B或Imm中的操作数进行算术或逻辑运算,并将结果放入临时寄存器ALUOutput中;或者根据操作码和功能,对A和Imm中的操作数进行有效地址计算,并将结果放入临时寄存器ALUOutput中。
- 访存(MEM):如果是加载指令,从存储器中读取数据,并放入临时寄存器LMD中;如果是存储指令,从B中读取数据,并写入存储器中;如果是分支指令,根据条件判断是否跳转,并更新PC。
- 写回(WB):如果是运算指令或加载指令,将ALUOutput或LMD中的结果写回目标寄存器;如果是其他类型的指令,则不进行写回操作。
(这些操作在上述字节码不太能看出来,因为这是针对汇编/机器码而设计的)
知道指令是由这么一个顺序来的了,那这和指令重排有什么关系呢?
你或许会发现,或许我们可以不用从上到下执行完所有指令,而是挑一些可以并发一起执行?
比如我们可以在执行一条指令的IF时还能执行别的指令的ID?
但有人不就会问CPU不就单核怎么做到?
CPU是单核,但每条指令用到的单元不一样啊,取指用PC寄存器,计算时就用ALU,互不干扰!
所以我们完全可以调整这些指令的执行顺序来做到最大化效率
而这种技术称之为指令流水线
而拥有像上面五层指令执行类别的CPU的流水线称之为五层流水线

这张图展示的处理器就能同时执行五条指令,原理就是充分利用了CPU中的其他单元,形成了一种“伪并行”
能够“预测”到后面的指令,并能找到可以提前用空闲的处理单元处理的指令提取执行
这样对计算机的提升非常大,以至于有CPU拥有1000多层的流水线
那CPU是怎么知道该怎么样找到可以并发的指令?
是通过分析“数据依赖”来发现那些可以并行运行
那既然存在数据依赖,那就一定会存在一种情况:下个指令必须用到上个指令的结果,且没有其他指令能插进来
那这样就会产生气泡,也称为**“打嗝”**:

一旦产生了气泡,会让后续操作周期延误,所以,为了维持流水线的高效率,CPU会尽力去进行指令重排来填补气泡
让能并发执行(互不干扰)的指令提前执行来填补气泡,避免延误执行周期
那么古尔丹,代价是什么呢?
尽管指令重组能保证语义不变,但不能保证在高并发条件下不会出错!
毕竟提前和延后修改共享变量都可能会引起不可预测的错误!
所以会采用synchronized 或 volatile 来针对性的避免这种情况
JVM中的指令重排与优化:
JVM中的指令重排和优化是发生在JIT编译阶段,而不是翻译成字节码阶段,网上很多博客都说错了!
仔细想一下也很正常,指令重排针对的是汇编机器码层面的操作,字节码根本接触不到
想要验证很简单,你找个程序,挑个能体现指令重组的程序,比较一下加了volatile和不加volatile的字节码,你会发现除了那个加了volatile的变量之外根本没区别
而且Java是解释+编译,一般情况下是由JVM一句一句照着字节码翻译成机器码走一步看一步,遇到有循环,执行多次的代码块就会用JIT对其进行编译优化,下次执行就直接调用JIT编译出来的机器码
所以很容易理解JVM的指令重排发生在JIT编译阶段
那JIT会干什么呢?
- JIT会根据JVM的不同(也就是底层的不同),适当的修改代码,调整顺序来迎合OS的流水线和指令重排。
- JIT也会给你写的屎山做优化,优化一些不必要的操作
举个例子:
package com.jitTest;public class test {static boolean noUse = true;public static void main(String[] args) {int cnt = 0;while(noUse){cnt++;if(cnt == 10000)break;}}
}
这里循环了10000次,肯定会触发JIT的热点代码优化
我们先下一个JITWatch
使用教程参考JITWatch很折腾?有这篇文章在可以放心大多数情况下,通过诸如javap等反编译工具来查看源码的字节码已经能够满足我 - 掘金
但是别照着它去自己编译dll,可直接在atzhangsan/file_loaded下载,JITWatch要下载源码手动编译,不能下jar!
之后我们用JITWatch截取其字节码和汇编代码:
字节码:
0: iconst_0 1: istore_1 2: getstatic #2 // Field noUse:Z5: ifeq 21 8: iinc 1, 1
11: iload_1
12: sipush 10000
15: if_icmpne 2
18: goto 21
21: return
可以发现根本没有优化掉noUse变量,这也证明之前的“代码重排发生在JIT编译而不是JVM编译成字节码”
接下来看汇编部分:
# {method} {0x000001a4d3fd44f0} 'main' '([Ljava/lang/String;)V' in 'com/jitTest/test'
# parm0: rdx:rdx = '[Ljava/lang/String;'
# [sp+0x20] (sp of caller)
[Entry Point]
0x000001a4b23e6140: sub $0x18,%rsp
0x000001a4b23e6147: mov %rbp,0x10(%rsp) ;*synchronization entry; - com.jitTest.test::main@-1 (line 6)
0x000001a4b23e614c: add $0x10,%rsp
0x000001a4b23e6150: pop %rbp
0x000001a4b23e6151: test %eax,-0x1e36157(%rip) # 0x000001a4b05b0000; {poll_return} *** SAFEPOINT POLL ***
0x000001a4b23e6157: retq
0x000001a4b23e6158: hlt
0x000001a4b23e6159: hlt
0x000001a4b23e615a: hlt
0x000001a4b23e615b: hlt
0x000001a4b23e615c: hlt
0x000001a4b23e615d: hlt
0x000001a4b23e615e: hlt
0x000001a4b23e615f: hlt
[Exception Handler]
[Stub Code]
0x000001a4b23e6160: jmpq 0x000001a4b2264620 ; {no_reloc}
[Deopt Handler Code]
0x000001a4b23e6165: callq 0x000001a4b23e616a
0x000001a4b23e616a: subq $0x5,(%rsp)
0x000001a4b23e616f: jmpq 0x000001a4b2006f40 ; {runtime_call}
0x000001a4b23e6174: hlt
0x000001a4b23e6175: hlt
0x000001a4b23e6176: hlt
0x000001a4b23e6177: hlt
其中的:
[Entry Point]
0x000001a4b23e6140: sub $0x18,%rsp
0x000001a4b23e6147: mov %rbp,0x10(%rsp) ;*synchronization entry; - com.jitTest.test::main@-1 (line 6)
0x000001a4b23e614c: add $0x10,%rsp
0x000001a4b23e6150: pop %rbp
0x000001a4b23e6151: test %eax,-0x1e36157(%rip) # 0x000001a4b05b0000; {poll_return} *** SAFEPOINT POLL ***
0x000001a4b23e6157: retq
便是函数部分,我们可以看到,其中唯一的比较函数就是 0x000001a4b23e6151: test %eax,-0x1e36157(%rip) ,代表着比较cnt是否到了10000,根本没有看见判断noUse变量是否为真
说明JIT编译时就已经发现noUse变量很no use,就将其删去了
对于指令重排,其实不太好测出来,复杂程序的汇编你看不出来,简单程序的汇编又被JIT优化后因为太简单就会按顺序执行
而且具体重组的方法是由你的底层决定,大头也是CPU的指令重排,JIT也是打个下手
但由此我们完全可以看出JIT可以对代码进行修改优化和重构来提升效率
JIT完全是Java的大爹
总结:
OS中的指令重排极大的提升了CPU性能,但也带来了并发风险
JVM中的JIT会在字节码转机器码时对代码进行优化修改以及重排,极大的提升了Java的速度,使其与编译执行语言速度相媲美
JIT太猛了…写的一个一百多行的测试屎山给优化到只有十几行…
相关文章:
关于JVM和OS中的指令重排以及JIT优化
关于JVM和OS中的指令重排以及JIT优化 前言: 这东西应该很重要才对,可是大多数博客都是以讹传讹,全是错误,尤其是JVM会对字节码进行重排都出来了,明明自己测一测就出来的东西,写出来误人子弟… 研究了两天&…...
微软推出首款量子计算芯片Majorana 1
全球首款拓扑架构量子芯片问世,2025年2月20日,经过近20年研究,微软推出了首款量子计算芯片Majorana 1,其宣传视频如本文末尾所示。 微软表示,开发Majorana 1需要创造一种全新的物质状态,即所谓的“拓扑体”…...
数组练习题总结
一、求出数组中的最大值let arr [1, 2, 5, 7];let max 0;for (i 0; i < arr.length; i) {// console.log(max);if (max < arr[i]) {max arr[i];}}console.log(max); 首先生成一个数组,设一个变量为保存最大值,写一个循环,在循环里写…...
Kotlin 中的 `reified` 关键字全解析:保留类型信息 + 优化高阶函数的双重魔法
在使用 Kotlin 编写泛型函数时,你是否遇到过这样的尴尬:你想判断某个对象是不是泛型类型 T,结果却发现代码根本编译不过?这是因为 Kotlin 和 Java 一样,泛型在运行时会被类型擦除。 为了解决这个问题,Kotl…...
在CPU服务器上部署Ollama和Dify的过程记录
在本指南中,我将详细介绍如何在CPU服务器上安装和配置Ollama模型服务和Dify平台,以及如何利用Docker实现这些服务的高效部署和迁移。本文分为三大部分:Ollama部署、Dify环境配置和Docker环境管理,适合需要在本地或私有环境中运行A…...
【计网】TCP 协议详解 与 常见面试题
三次握手、四次挥手的常见面试题 不用死记,只需要清楚三次握手,四次挥手的流程,回答的时候心里要记住,假设网络是不可靠的 问题(1):为什么关闭连接时需要四次挥手,而建立连接却只要三次握手? 关…...
Java 基础-31-枚举-认识枚举
在Java编程语言中,枚举(Enum)是一种特殊的类,它允许一组固定的常量。它们非常适合用来表示一组固定的值,比如星期几、季节、颜色等。枚举自Java 5开始引入,为定义常量提供了一种更强大和方便的方式。本文将…...
7.4 SVD 的几何背景
一、SVD 的几何解释 SVD 将矩阵分解成三个矩阵的乘积: ( 正交矩阵 ) ( 对角矩阵 ) ( 正交矩阵 ) (\pmb{正交矩阵})\times(\pmb{对角矩阵})(\pmb{正交矩阵}) (正交矩阵)(对角矩阵)(正交矩阵),用几何语言表述其几何背景: ( 旋转 ) ( 伸缩 )…...
低延迟云网络的核心技术
低延迟云网络通过架构优化、协议创新、硬件加速等多维度技术手段,将数据传输延迟降低至毫秒级甚至微秒级。 1. 网络架构优化 1.1 扁平化网络Leaf-Spine 架构 减少网络层级,缩短数据转发路径(如数据中心内部一跳可达)。 扁平化网络Leaf-Spine(叶子-脊椎)架构是一种现代…...
C++的多态-上
目录 多态的概念 多态的定义及实现 1.虚函数 2. 多态的实现 2.1.多态构成条件 2.2.虚函数重写的两个例外 (1)协变(基类与派生类虚函数返回值类型不同) (2)析构函数的重写(基类与派生类析构函数的名字不同) 2.3.多态的实现 2.4.多态在析构函数中的应用 2.5.多态构成条…...
内存与显存:从同根生到殊途异路的科技演进
在现代计算机的世界里,内存和显存是两个不可或缺的硬件组件。它们看似功能相近,却在发展历程中逐渐分道扬镳,各自服务于不同的计算需求。今天,我们将从一根内存条和一块显卡入手,深入探讨内存与显存的异同,…...
手搓多模态-04 归一化介绍
在机器学习中,归一化是一个非常重要的工具,它能帮助我们加速训练的速度。在我们前面的SiglipVisionTransformer 中,也有用到归一化层,如下代码所示: class SiglipVisionTransformer(nn.Module): ##视觉模型的第二层&am…...
【C++】第八节—string类(上)——详解+代码示例
hello,又见面了! 云边有个稻草人-CSDN博客 C_云边有个稻草人的博客-CSDN博客——C专栏(质量分高达97!) 菜鸟进化中。。。 目录 一、为什么要学习string类? 1.1 C语言中的字符串 1.2 面试题(暂不做讲解) …...
Java 数组与 ArrayList 核心区别解析:从源码到实战!!!
🌟 Java 数组与 ArrayList 核心区别解析:从源码到实战 💡 Java 开发者必读! 数组(Array)和 ArrayList 是 Java 中最常用的数据存储结构,但它们的底层设计、性能表现和适用场景差异显著。本文通…...
【易飞】易飞批量选择品号处理方法,工作效率提升300%
开窗选择品号方式要么手动输入,要么以什么开头、包含、从A物料到B物料查询后返回的有规律的品号。对于没有规律且大量品号的处理方式是否有便捷的方法呢? 尤其在通常在查询多阶材料清单,查询库存明细表,整批变更元件等如品号无规律情况下,只能一个个选择,无法通过EXCEL方…...
【最新版】啦啦外卖v64系统独立版源码+全部小程序APP端+安装教程
一.系统介绍 啦啦外卖跑腿平台独立版,使用的都知道该系统功能非常强大,应该说是目前外卖平台功能最全的一套系统。主要是功能非常多,拿来即用,包括客户端小程序、配送端小程序、商户端小程序,还有对应四个端的APP源码…...
iproute2 工具集使用详解
目录 一、iproute2 核心命令:ip二、常用功能详解1. 管理网络接口(link 对象)2. 管理 IP 地址(address 对象)3. 管理路由表(route 对象)4. 管理 ARP 和邻居缓存(neigh 对象࿰…...
项目总结之常问的一些问题
1.项目功能介绍,重难点 重难点: mock工具使用(涉及到的三方接口过多,由于网络等原因无法调通,所以测试的时候,采用mock工具来模拟返回接口真正调用后响应数据) 2.项目负责哪部分?…...
C语言查漏补缺:占位符篇
占位符篇 1. 整数类型2. 字符类型3. 浮点数类型4. 指针类型5. 字符串6. 修饰符 1. 整数类型 %d / %i:用于 int(有符号十进制整数)。int num -42; printf("%d", num); // 输出: -42%u:用于 unsigned int(无…...
cut命令用法
cut 是 Linux/Unix 系统中一个用于按列提取文本内容的命令,常用于处理结构化文本(如 CSV、日志、配置文件等)。它通过分隔符、字符位置或字节位置来切割文本,提取指定部分。 核心功能 按字段(列)提取&#…...
java 正则表达式优化
1,什么是正则表达式 正则表达式使用一些特定的元字符来检索、匹配以及替换符合规则的字符串。 构造正则表达式语法的元字符,由普通字符、标准字符、限定字符(量词)、定位字符(边界字符)组成 普通字符 字母[…...
AD(Altium Designer)更换PCB文件的器件封装
一、确定是否拥有想换的器件PCB封装 1.1 打开现有的原理图 1.2 确定是否拥有想换的器件PCB文件 1.2.1 如果有 按照1.3进行切换器件PCB封装 1.2.2 如果没有 按照如下链接进行添加 AD(Altium Designer)已有封装库的基础上添加器件封装-CSDN博客https://blog.csdn.net/XU15…...
【文献研究】含硼钢中BN表面偏析对可镀性的影响
《B 添加钢的溶融 Zn めっき性に及ぼす BN 表面析出の影響》由JFE公司田原大輔等人撰写。研究聚焦 B 添加钢在低露点退火时 BN 形成对镀锌性的影响,对汽车用高强度钢镀锌工艺优化意义重大。通过多组对比实验,结合多种分析手段,明确了相关因素…...
React学习-css
W3Schools Tryit Editor CSS 教程 CSS 规则由两个主要的部分构成:选择器,以及一条或多条声明: p { /* 这是个注释 */ color:red; text-align:center; }选择器 CSS Id: #para1{ text-align:center; color:red; } Class: .center {text-align:center;} p…...
AIP-215 API特定proto
编号215原文链接AIP-215: API-specific protos状态批准创建日期2018-10-01更新日期2018-10-01 API通常使用API特定proto定义,偶尔依赖通用组件。保持API相互隔离可以避免版本问题和客户端库打包问题。 指南 所有特定于某个API的protos 必须 位于带有主版本号的包…...
C++ 获取一整行(一行)字符串并转换为数字
代码很简单,主要是自己总是忘记,记录一下: #include <iostream> #include <cstdlib> #include <cstring>#include <string> #include <vector> #include <sstream>using namespace std;void print_int_…...
数据分析-Excel-学习笔记Day1
Day1 复现报表聚合函数:日期联动快速定位区域SUMIF函数SUMIFS函数环比、同比计算IFERROR函数混合引用单元格格式总结汇报 拿到一个Excel表格,首先要看这个表格的构成(包含了哪些数据),几行几列,每一列的名称…...
树莓派PICO 设备烧录成cmsis dap
文章目录 1. 实际操作2. IO连接 1. 实际操作 2. IO连接...
【数据结构】图的存储
目录 邻接矩阵 表示方法 代码定义 结构特点与度的信息 邻接表 表示方法 代码定义 结构特点与度的信息 十字链表 表示方法 第二步,将顶点x的firstIn域与所有headvex域为x的弧连起来。 结构特点与度的信息 邻接多重表 表示方法 结构特点与度的信息 图…...
如何解决uniapp打包安卓只出现功能栏而无数据的问题
如何解决uniapp打包安卓只出现功能栏而无数据的问题 经验来自:关于Vue3中调试APP触发异常:exception:white screen cause create instanceContext failed,check js stack -> at useStore (app-service.js:2309:15)解决方案 - 甲辰哥来帮你算命 - 博客…...
