当前位置: 首页 > news >正文

深入理解Java的 JIT(即时编译器)

🧑 博主简介:CSDN博客专家历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程高并发设计Springboot和微服务,熟悉LinuxESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
技术合作请加本人wx(注明来自csdn):foreast_sea

在这里插入图片描述


在这里插入图片描述

文章目录

  • 深入理解Java的 JIT(即时编译器)
    • JVM 的编译器
      • Client Compiler
      • Server Compiler
        • C2 Compiler
        • Graal Compiler
    • JVM 的分层编译
    • JIT 的触发
    • JIT 的编译优化
      • 中间表达形式
        • C1 的 HIR
        • C2 的 Sea-of-Nodes IR
      • 方法内联
      • 逃逸分析
      • 窥孔优化与寄存器分配
    • 小结

深入理解Java的 JIT(即时编译器)

本篇内容我们将深入探讨 JIT(即时编译器),包括 JVM 的编译器JVM 的分层编译JIT 的触发JIT 的编译优化等内容。

Java 为了提升 运行时的性能,JVM 引入了 JIT,也就是即时编译Just In Time)技术。

Java 代码首先被编译为字节码,JVM 在运行时通过解释器执行字节码。当某部分的代码被频繁执行时,JIT 会将这些热点代码编译为机器码,以此来提高程序的执行效率。

那为什么 JIT 就能提高程序的执行效率呢,解释器不也是将字节码翻译为机器码交给操作系统执行吗?

解释器在执行程序时,对于每一条字节码指令,都需要进行一次解释过程,然后执行相应的机器指令。这个过程在每次执行时都会重复进行,因为解释器不会记住之前的解释结果。

与此相对,JIT 会将频繁执行的字节码编译成机器码。这个过程只发生一次。一旦字节码被编译成机器码,之后每次执行这部分代码时,直接执行对应的机器码,无需再次解释。

除此之外,JIT 生成的机器码更接近底层,能够更有效地利用 CPU 和内存等资源,同时,JIT 能够在运行时根据实际情况对代码进行优化(如内联、循环展开、分支预测优化等),这些优化是在机器码级别上进行的,可以显著提升执行效率。

换句话说,解释器是一个循规蹈矩的人,每次都要按照规则来执行,而 JIT 是一个“偷奸耍滑”的人,他会根据实际情况来做出最优的选择。

好,我们再来梳理一下。

Java 的执行过程分为两步,第一步由 javac 将源码编译成字节码,在这个过程中会进行词法分析、语法分析、语义分析。

第二步,解释器会逐行解释字节码并执行,在解释执行的过程中,JVM 会对程序运行时的信息进行收集,在这些信息的基础上,JIT 会逐渐发挥作用,它会把字节码编译成机器码,但不是所有的代码都会被编译,只有被 JVM 认定为热点代码,才会被编译。

怎么样才会被认为是热点代码呢

JVM 中有一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被认定为热点代码,然后编译存入 codeCache 中。当下次执行时,再遇到这段代码,就会从 codeCache 中直接读取机器码,然后执行,以此来提升程序运行的性能。

整体的执行过程大致如下图所示:

这里的 codeCache 让我想起了 Redis,Redis 也是将热点数据存储在内存中,以此来提升访问速度。

OK,解释清楚了 JIT 的原理,我们来看看 JIT 的实现。

JVM 的编译器

JVM 中集成了两种编译器,一种是 Client Compiler,另外一种是 Server Compiler。

Client Compiler 注重启动速度和局部的优化,Server Compiler 则更加关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会慢一些。

两种编译器相辅相成,互为臂膀,共同把 JVM 的性能带到了一个新的高度。

Client Compiler

就那虚拟机中的太子 HotSpot 来说吧,它就带有一个 Client Compiler,被称为 C1 编译器,启动速度极快。

C1 通常会做这三件事:

①、局部简单可靠的优化,比如在字节码上进行一些基础优化,方法内联、常量传播等。

我们来举例看一下什么是方法内联,假设我们有两个简单的方法:

public class Example {public int add(int a, int b) {return a + b;}public void run() {int result = add(5, 3);System.out.println(result);}
}

在执行 run 方法时,会调用 add 方法,方法内联优化后,会将 add 方法的字节码直接插入到 run 方法中,这样就不用再去调用 add 方法了,直接执行 run 方法就可以了。

public class Example {public void run() {int a = 5;int b = 3;int result = a + b;  // 这里是内联后的 add 方法体System.out.println(result);}
}

②、将字节码编译成 HIR(High-level Intermediate Representation),别计较它中文名叫什么,我觉得与其死板的翻译,不如就记住它叫 HIR,一种比较接近源代码的形式。

通过借助 HIR 我们可以实现冗余代码消除、死代码删除等编译优化工作,我们同样通过代码来看一下。

public class OptimizationExample {public int calculate(int x, int y) {int a = x + y;int b = x + y;  // 冗余计算return a * b;}
}

很明显,上面的代码中,b 的值是可以直接通过 a 的值计算出来的,所以 b 的计算就是冗余的,我们可以通过 HIR 来消除这种冗余计算。

public class OptimizationExample {public int calculate(int x, int y) {int a = x + y;return a * a;  // 使用一个计算结果}
}

在 HIR 优化阶段,编译器识别到 x + y 的计算是冗余的,因此它将第二次计算的结果用第一次的结果替换。

③、最后将 HIR 转换成 LIR(Low-level Intermediate Representation),比较接近机器码了。这期间会做一些寄存器分配、窥孔优化等。

寄存器分配是指在编译时将程序中的变量分配到 CPU 的寄存器上。由于寄存器的访问速度远快于内存,因此合理的寄存器分配可以显著提高程序的执行效率。

来看这段代码:

int a = 5;
int b = 10;
int c = a + b;
System.out.println(c);

在没有寄存器优化的情况下,编译器会将变量 a、b、c 分配到内存中,然后在执行时,再从内存中读取变量的值。有了寄存器分配优化呢?

R1 = 5     // 将 5 赋值给寄存器 R1
R2 = 10    // 将 10 赋值给寄存器 R2
R3 = R1 + R2 // 将 R1 和 R2 的和赋值给寄存器 R3

这样,变量 a、b、c 就被分配到了寄存器 R1、R2、R3 上,而不是内存中,寄存器的访问速度远快于内存,所以这样的优化可以提高程序的执行效率。

窥孔优化(Peephole Optimization)是一种在生成机器码阶段进行的局部优化技术。编译器“窥视”一小段生成的机器码,并尝试找出并替换更高效的指令序列。

假设有这样一段简单的机器码:

MOV R1, 0
ADD R1, 5

这段代码首先将寄存器 R1 置零,然后再将 5 加到 R1 上,窥孔优化会将这两条指令合并成一条:

MOV R1, 5

这样,仅用一条指令就完成了同样的操作,显然会提高代码执行的效率。

Server Compiler

Server Compiler 主要关注一些编译耗时较长的全局优化,甚至会还会根据程序运行的信息进行一些不可靠的激进优化。这种编译器的启动时间长,适用于长时间运行的后台程序,它的性能通常比 Client Compiler 高 30%以上。目前,Hotspot 虚拟机中使用的 Server Compiler 有两种:C2 和 Graal。

C2 Compiler

Hotspot 中,默认的 Server Compiler 是 C2 编译器。

C2 编译器在进行编译优化时,会使用一种控制流与数据流结合的图数据结构,称为 Ideal Graph,我愿称之为“理想图”。

Ideal Graph 表示当前程序的数据流向和指令间的依赖关系,依靠这种图结构,某些优化步骤(尤其是涉及浮动代码块的优化步骤)会变得不那么复杂。

解析字节码的时候,C2 会向一个空的 Graph 中添加节点,Graph 中的节点通常对应一个指令块,每个指令块包含多条相关联的指令,JVM 会利用一些优化技术对这些指令进行优化,比如 Global Value Numbering、常量折叠等,解析结束后,还会进行一些死代码剔除的操作。

生成 Ideal Graph 后,会在这个基础上结合收集到的程序运行信息来进行一些全局的优化。

无论是否进行全局优化,Ideal Graph 都会被转化为一种更接近机器层面的 MachNode Graph,最后编译的机器码就是从 MachNode Graph 中得到的。

Graal Compiler

从 JDK 9 开始,Hotspot 中集成了一种新的 Server Compiler,也就是 Graal 编译器。相比 C2,Graal 有这样几种关键特性:

①、JVM 会在解释执行的时候收集程序运行的各种信息,然后根据这些信息进行一些基于预测的激进优化,比如分支预测,根据程序不同分支的运行概率,选择性地编译一些概率较大的分支。Graal 比 C2 更加青睐这种优化,所以 Graal 的峰值性能通常要比 C2 更好。

②、与 C2(主要用 C++ 编写)不同,Graal 使用 Java 语言编写。这样做的好处是,Graal 可以直接使用 JVM 的内存管理机制,不需要像 C2 那样自己实现内存管理,这样就可以避免一些内存管理上的问题。

③、Graal 引入了许多现代化的编译优化技术,例如更复杂的内联策略、循环优化等,这些在某些情况下可以比 C2 产生更优化的代码。

④、改进的逃逸分析有助于更好地进行栈上分配和锁消除,从而提升性能。

⑤、Graal 不仅能作为 JIT 编译器使用,还支持 Ahead-of-Time(AOT)编译,这有助于减少 Java 应用的启动时间和内存占用。

Graal 编译器可以通过 JVM 参数-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 启用。当启用时,它将替换掉 HotSpot 中的 C2,并响应原本由 C2 负责的编译请求。

JVM 的分层编译

Java 7 引入了分层编译的概念,它结合了 C1 和 C2 的优势,追求启动速度和峰值性能的一个平衡。分层编译将 JVM 的执行状态分为了五个层次。五个层级分别是:

Java 7 中引入的分层编译(Tiered Compilation)确实是一种结合了 C1 编译器(Client Compiler)和 C2 编译器(Server Compiler)优势的技术。分层编译旨在优化 Java 程序的启动速度和长期运行时的性能。这一机制通过在不同的层级应用不同的编译策略,以达到快速启动和最高性能的平衡。在 HotSpot JVM 中,分层编译将程序执行状态分为五个层次:

  1. 层级 0 - 解释器(Interpreter):这是程序最初执行的阶段,代码通过解释器逐行解释执行。这一阶段的目的是尽快开始执行而不等待编译完成。

  2. 层级 1 - C1 编译器带有轻量级优化(C1 with Simple Optimizations):在这一层级,代码首次由 C1 编译器编译,应用了一些基本的优化,如方法内联。这一阶段的编译速度较快,能迅速提供优于解释执行的性能。

  3. 层级 2 - C1 编译器带有完整优化(C1 with Full Optimizations):此层级仍由 C1 编译器处理,但应用了更多优化技术,如逃逸分析。虽然这些优化需要更长的编译时间,但能进一步提升运行性能。

  4. 层级 3 - C1 编译器带有分析数据收集(C1 with Profiling): 在这个层级,C1 编译器除了执行优化,还收集方法执行的详细分析数据(如分支频率、热点代码等)。这些数据将用于 C2 编译器的后续优化。

  5. 层级 4 - C2 编译器优化(C2 Optimizations):最终阶段由 C2 编译器处理,它使用收集的分析数据进行深入优化。C2 编译器的优化更加彻底和复杂,适用于长时间运行的代码,能够提供最佳的运行性能。

下图中列举了几种常见的编译路径:

1)图中第 ① 条路径,代表编译的一般情况,热点方法从解释执行到被 3 层的 C1 编译,最后被 4 层的 C2 编译。

2)如果方法比较小(比如 getter/setter),3 层的 profiling 没有收集到有价值的数据,JVM 就会断定该方法对于 C1 代码和 C2 代码的执行效率相同,就会执行图中第 ② 条路径。

在这种情况下,JVM 会在 3 层编译之后,放弃进入 C2 编译,直接选择用 1 层的 C1 编译运行。

3)在 C1 忙碌的情况下,执行图中第 ③ 条路径,在解释执行过程中对程序进行 profiling,根据信息直接由第 4 层的 C2 编译。

4)C1 中的执行效率是 1 层>2 层>3 层,第 3 层一般要比第 2 层慢 35%以上,所以在 C2 忙碌的情况下,执行图中第 ④ 条路径。这时方法会被 2 层的 C1 编译,然后再被 3 层的 C1 编译,以减少方法在 3 层的执行时间。

5)如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第 ⑤ 条执行路径代表的就是反优化。

分层编译通过在不同的阶段应用不同程度的优化,既提供了较快的应用启动时间,又确保了长时间运行的应用能达到峰值性能。这种动态适应的编译策略是 Java 平台持续优化性能的关键手段之一。

从 JDK 8 开始,JVM 默认开启分层编译。

JIT 的触发

JVM 根据方法的调用次数以及循环回边的执行次数来触发 JIT。

循环回边是一个控制流图中的概念,程序中可以简单理解为往回跳转的指令,比如下面这段代码:

public void nlp(Object obj) {int sum = 0;for (int i = 0; i < 200; i++) {sum += i;}
}

上面这段代码经过编译生成下面的字节码。

public void nlp(java.lang.Object);Code:0: iconst_01: istore_12: iconst_03: istore_24: iload_25: sipush        2008: if_icmpge     2111: iload_112: iload_213: iadd14: istore_115: iinc          2, 118: goto          421: return

其中,偏移量为 18 的字节码将往回跳至偏移量为 4 的字节码中。在解释执行时,每当运行一次该指令,JVM 便会将该方法的循环回边计数器加 1。

在即时编译过程中,编译器会识别循环的头部和尾部。上面这段字节码中,循环体的头部和尾部分别为偏移量为 11 的字节码和偏移量为 15 的字节码。编译器将在循环体结尾增加循环回边计数器的代码,来对循环进行计数。

当方法的调用次数和循环回边的次数的和,超过由参数 -XX:CompileThreshold 指定的阈值时,就会触发即时编译。

C1 默认值为 1500;C2 默认值为 10000。

开启分层编译的情况下,-XX:CompileThreshold 参数设置的阈值将会失效,触发及时编译会由以下的条件来判断:

  • 方法调用次数大于由参数-XX:TierXInvocationThreshold 指定的阈值乘以系数。
  • 方法调用次数大于由参数-XX:TierXMINInvocationThreshold 指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold 指定的阈值乘以系数时。

分层编译触发条件公式(i 为调用次数,b 是循环回边次数,s 是系数):

i > TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s  && i + b > TierXCompileThreshold * s)

满足其中一个条件就会触发即时编译,并且 JVM 会根据当前的编译方法数以及编译线程数动态调整系数 s。

JIT 的编译优化

即时编译器会对正在运行的程序进行一系列优化,包括:

  • 字节码解析过程中的分析
  • 根据编译过程中代码的一些中间形式来做局部优化
  • 根据程序依赖图进行全局优化

最后才会生成机器码。

中间表达形式

在编译原理中,通常会把编译器分为前端和后端,前端编译经过词法分析、语法分析、语义分析生成中间表达形式 IR(Intermediate Representation),后端会对 IR 进行优化,生成目标代码。

Java 字节码就是一种 IR,但是字节码的结构复杂,也不适合做全局的分析优化。

现代编译器一般采用图结构的 IR,也就是所谓的静态单赋值——Static Single Assignment,SSA 是目前比较常用的一种 IR。这种 IR 的特点是每个变量只能被赋值一次,而且只有当变量被赋值之后才能使用。

举个例子(前面也讲过,这里再强调一遍):

{a = 1;a = 2;b = a;
}

我们可以轻易地发现 a = 1 的赋值是冗余的。传统的编译器需要借助数据流分析,从后至前依次确认哪些变量的值被覆盖掉了。不过,如果借助了 SSA IR,编译器则可以很容易识别冗余赋值。

上面代码的 SSA IR 形式的伪代码可以表示为:

{a_1 = 1;a_2 = 2;b_1 = a_2;
}

由于 SSA IR 中每个变量只能赋值一次,所以代码中的 a 在 SSA IR 中会分成 a_1、a_2 两个变量来赋值,这样编译器就可以很容易通过扫描这些变量来发现 a_1 的赋值后并没有使用,由此认定该赋值是冗余的。

除此之外,SSA IR 对其他优化方式也有很大的帮助,例如下面这个死代码删除(Dead Code Elimination)的例子:

public void DeadCodeElimination{int a = 2;int b = 0if(2 > 1){a = 1;} else{b = 2;}add(a,b)
}

可以得到 SSA IR 伪代码:

a_1 = 2;
b_1 = 0
if true:a_2 = 1;
elseb_2 = 2;
add(a,b)

编译器通过执行字节码可以发现 else 分支不会被执行。经过死代码删除后就可以得到代码:

public void DeadCodeElimination{int a = 1;int b = 0;add(a,b)
}

我们可以将编译器的每一种优化看成一个图优化算法,它接收一个 IR 图,并输出经过转换后的 IR 图。编译器优化的过程就是一个个图节点的优化串联起来的。

C1 的 HIR

前文提到了 C1 编译器内部使用了高级中间表达形式 HIR,低级中间表达形式 LIR 来进行各种优化,这两种 IR 都是 SSA 形式的。

HIR 是由很多基本块(Basic Block)组成的控制流图结构,每个块包含很多 SSA 形式的指令。基本块的结构如下图所示:

其中,predecessors 表示前驱基本块,由于前驱可能是多个,所以是 BlockList 结构,由多个 BlockBegin 组成的可扩容数组。

同样,successors 表示多个后继基本块 BlockEnd。

除了这两部分就是主体块,里面包含程序执行的指令和一个 next 指针,指向下一个执行的主体块。

从字节码到 HIR 的构造最终调用的是 GraphBuilder,GraphBuilder 会遍历字节码,将所有代码基本块存储为一个链表结构,但是这个时候的基本块只有 BlockBegin,不包括具体的指令。

第二步 GraphBuilder 会用一个 ValueStack 作为操作数栈和局部变量表,模拟执行字节码,构造出对应的 HIR,填充之前空的基本块,这里给出简单字节码块构造 HIR 的过程示例,如下所示:

字节码                     Local Value             operand stack              HIR
5: iload_1                  [i1,i2]                 [i1]
6: iload_2                  [i1,i2]                 [i1,i2]................................................   i3: i1 * i2
7: imul
8: istore_3                 [i1,i2,i3]              [i3]

可以看出,当执行 iload_1 时,操作数栈压入变量 i1,执行 iload_2 时,操作数栈压入变量 i2,执行相乘指令 imul 时弹出栈顶两个值,构造出 HIR i3 : i1 * i2,生成的 i3 入栈。

C1 编译器的大部分优化工作都是在 HIR 之上完成的。当优化完成之后它会将 HIR 转化为 LIR,LIR 和 HIR 类似,也是一种编译器内部用到的 IR,HIR 通过优化消除一些中间节点就可以生成 LIR,形式上更加简化。

C2 的 Sea-of-Nodes IR

C2 编译器中的 Ideal Graph 采用的是一种名为 Sea-of-Nodes 中间表达形式,同样也是 SSA 形式。

它最大的特点是去除了变量的概念,直接采用值来进行运算。为了方便理解,可以利用 IR 可视化工具 Ideal Graph Visualizer(IGV),来展示具体的 IR 图。比如下面这段代码:

public static int foo(int count) {int sum = 0;for (int i = 0; i < count; i++) {sum += i;}return sum;
}

对应的 IR 图如下所示:

B0 基本块中 0 号 Start 节点是方法入口,B3 中 21 号 Return 节点是方法出口。

红色加粗线条为控制流,蓝色线条为数据流,其他颜色的线条则是特殊的控制流或数据流。

被控制流所连接的是固定节点,其他的则是浮动节点。

依赖于这种图结构,通过收集程序运行的信息,JVM 可以通过 Schedule 那些浮动节点,从而获得最好的编译效果。

方法内联

来看下面这段代码:

public static boolean flag = true;
public static int value0 = 0;
public static int value1 = 1;public static int foo(int value) {int result = bar(flag);if (result != 0) {return result;} else {return value;}
}public static int bar(boolean flag) {return flag ? value0 : value1;
}

来看一下 bar 方法的 IR 图:

内联后的 IR 图:

内联将被调用方法的 IR 图节点复制到调用者方法的 IR 图中。在这个例子中,bar 方法的 IR 图中的 0 号 Start 节点被复制到了 foo 方法的 IR 图中,从而避免了方法调用的开销。

逃逸分析

逃逸分析是 JIT 用于优化内存管理和同步操作的重要技术。通过分析对象是否逃逸到方法或线程的外部,编译器可以做出更智能的存储和同步决策。

逃逸分析通常是在方法内联的基础上进行的,JIT 可以根据逃逸分析的结果进行诸如锁消除、栈上分配以及标量替换的优化。

下面这段代码的就是对象未逃逸的例子:

public class Example {public static void main(String[] args) {example();}public static void example() {Foo foo = new Foo();Bar bar = new Bar();bar.setFoo(foo);}
}class Foo {}class Bar {private Foo foo;public void setFoo(Foo foo) {this.foo = foo;}
}

在这个例子中,example 方法创建了两个对象:FooBar。然后,Bar 对象通过 setFoo 方法引用了 Foo 对象。

  1. Foo 对象的逃逸情况

    • Foo 对象被创建并传递给 Bar 对象的 setFoo 方法。
    • 一旦 setFoo 方法被调用,Foo 对象的引用存储在 Bar 对象的实例变量 foo 中。
    • 但是,Bar 对象本身在 example 方法结束后就不再使用。
    • 这意味着即使 Foo 对象的引用被存储在另一个对象中,但由于 Bar 对象本身也不会逃逸出 example 方法,因此 Foo 对象实际上也没有逃逸。
  2. Bar 对象的逃逸情况

    • Bar 对象在 example 方法中被创建并使用,但之后没有被传递到其他方法或返回。
    • 因此,Bar 对象也没有逃逸出 example 方法。

根据逃逸分析的结果,JIT 可能做出以下优化决策:

①、锁消除:如果 FooBar 类中有同步块(使用 synchronized),由于对象没有逃逸,编译器可以安全地消除这些锁操作。

②、栈上分配:由于 FooBar 对象都没有逃逸到方法之外,编译器可以选择在栈上分配这两个对象,而非在堆上分配。这样可以提高内存分配的效率,并减少垃圾收集器的压力。

堆和栈的区别可以查看这篇内容:JVM 的内存数据区

我们都知道 Java 的对象是在堆上分配的,而堆是对所有对象可见的。同时,JVM 需要对所分配的堆内存进行管理,并且在对象不再被引用时回收其所占据的内存。

如果逃逸分析能够证明某些新建的对象不逃逸,那么 JVM 完全可以将其分配至栈上,并且在 new 语句所在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。

这样一来,我们便无须借助垃圾收集器来处理不再被引用的对象。

不过 Hotspot 并没有进行实际的栈上分配,而是使用了标量替换的技术。

③、标量替换(Scalar Replacement)

标量替换是一种优化技术,其中编译器将一个聚合对象分解为其各个字段。如果这个对象没有逃逸出方法,那么它的各个字段可以视为独立的局部变量。

这种技术允许编译器进行更细粒度的优化,如更好的寄存器分配和减少不必要的内存分配。

考虑这段代码:

public class Example {@AllArgsConstructorstatic class Cat {int age;int weight;}public static void example() {Cat cat = new Cat(1, 10);addAgeAndWeight(cat.age, cat.weight);}public static void addAgeAndWeight(int age, int weight) {// 对年龄和体重进行一些操作}public static void main(String[] args) {example();}
}
  1. 对象的使用范围

    • example 方法中创建了一个 Cat 对象,并将其字段 ageweight 传递给了 addAgeAndWeight 方法。
    • Cat 对象在 example 方法中创建且只在该方法中使用,没有被传递到方法外部或赋值给外部引用。
  2. 逃逸分析

    • 由于 Cat 对象在方法外部没有引用,它没有逃逸出 example 方法的作用域。
    • 这意味着 Cat 对象是一个局部对象,适合进行标量替换。
  3. 标量替换的应用

    • JVM 的 JIT 编译器会分析 Cat 对象的使用情况。基于逃逸分析,编译器可以决定不在堆上分配 Cat 对象,而是将其分解为两个独立的局部变量 ageweight
    • 这样,原本由 Cat 对象占用的堆空间就被节省下来,而且减少了垃圾回收的压力。
  4. 优化后的执行

    • 在执行 example 方法时,Cat 对象的字段 ageweight 直接作为栈上的局部变量处理,避免了堆分配。

标量替换后的伪代码如下所示:

public class Example {public static void example() {int catAge = 1;int catWeight = 10;addAgeAndWeight(catAge, catWeight);}public static void addAgeAndWeight(int age, int weight) {// 方法实现}
}

可以看到,Cat 对象被分解为两个局部变量 catAgecatWeight,并且直接作为参数传递给了 addAgeAndWeight 方法。

窥孔优化与寄存器分配

前面我们也简单分析了一下窥孔优化与寄存器分配,相信大家对这两个概念都有了一定的了解,这里简单总结下。

窥孔优化就是将编译器所生成的中间代码中的某些组合替换为效率更高的指令组,比如强度削减、常数合并等,看下面这个例子就是一个强度削减的例子:

y1=x1*3  
经过强度削减后得到  
y1=(x1<<1)+x1

编译器使用移位和加法削减乘法的强度,使用更高效率的指令组。

寄存器分配也是一种编译的优化手段,在 C2 编译器中普遍的使用。它是通过把频繁使用的变量保存在寄存器中,CPU 访问寄存器的速度比内存快得多,可以提升程序的运行速度。

经过寄存器分配和窥孔优化之后,程序就会被转换成机器码保存在 codeCache 中。

小结

本文主要介绍了 JIT 即时编译的原理以及编译优化的过程,包括:

  • JIT 的触发条件
  • JIT 的编译优化
  • JIT 的编译器

JIT 是 JVM 的重要组成部分,它可以根据程序运行的情况,对热点代码进行编译优化,从而提升程序的运行效率。

相关文章:

深入理解Java的 JIT(即时编译器)

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…...

数据库技术文档撰写:全方位剖析

在技术的浩瀚海洋中&#xff0c;一份优秀的技术文档宛如精准的航海图。它是知识传承的载体&#xff0c;是团队协作的桥梁&#xff0c;更是产品成功的幕后英雄。然而&#xff0c;打造这样一份出色的技术文档并非易事。你是否在为如何清晰阐释复杂技术而苦恼&#xff1f;是否纠结…...

设计模式之原型模式:深入浅出讲解对象克隆

~犬&#x1f4f0;余~ “我欲贱而贵&#xff0c;愚而智&#xff0c;贫而富&#xff0c;可乎&#xff1f; 曰&#xff1a;其唯学乎” 原型模式概述 在我们的日常生活中&#xff0c;经常会遇到"复制"这样的场景。比如我们在准备文件时&#xff0c;常常会复印一份原件&a…...

centos 查看版本

在 CentOS 中&#xff0c;查看系统版本有多种方法。以下是几种常用的方法&#xff1a; 方法 1&#xff1a;使用 cat 命令查看 /etc/centos-release 文件 cat /etc/centos-release 这个文件包含了 CentOS 的版本信息。例如&#xff0c;输出可能是&#xff1a; CentOS Linux rel…...

如何本地存储中的文件路径

文章目录 1. 概念介绍2. 实现方法3. 示例代码我们在上一章回中介绍了"如何实现本地存储"相关的内容,本章回中将介绍如何实现文件存储.闲话休提,让我们一起Talk Flutter吧。 1. 概念介绍 我们在上一章回中介绍的本地存储只能存储dart语言中基本类型的数值,如果遇到…...

服务器加固

1.服务器密码复杂度 密码最小长度&#xff0c;密码复杂度策略 vim /etc/pam.d/system-auth --------------- #密码配置 #ucredit&#xff1a;大写字母个数&#xff1b;lcredit&#xff1a;小写字母个数&#xff1b;dcredit&#xff1a;数字个数&#xff1b;ocredit&#xff1a;…...

MongoDB change stream实战

什么是 Chang Stream Change Stream指数据的变化事件流&#xff0c;MongoDB从3.6版本开始提供订阅数据变更的功能。 Change Stream 是 MongoDB 用于实现变更追踪的解决方案&#xff0c;类似于关系数据库的触发器&#xff0c;但原理不完全相同&#xff1a; Change Stream 的实…...

TSWIKI知识库软件

TSWIKI 知识库软件介绍 推荐一个适合本地化部署、自托管的知识库软件 TSWIKI介绍 tswiki 是一个适合小团队、个人的知识库、资料管理的软件&#xff0c;所有数据均本地化存储。可以本地化、私有云部署&#xff0c;安装简单。在线预览。 主要功能说明 1、简化的软件依赖和安…...

【Linux课程学习】第十九弹---深入理解进程间通信---匿名管道,命名管道,多匿名管道的BUG

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;Linux课程学习 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 ​ Linux学习笔记&#xff1a; https://blog.csdn.n…...

【C语言】库函数常见的陷阱与缺陷(1):字符串处理函数

目录 一、 strcpy 函数 1.1. 功能与常见用法 1.2. 陷阱与缺陷 1.3. 安全替代 1.4. 代码示例 二、strcat 函数 2.1. 功能与常见用法 2.2. 陷阱与缺陷 2.3. 安全替代 2.4. 代码示例 三、strcmp 函数 3.1. 功能与常见用法 3.2. 陷阱与缺陷 3.3. 安全替代 3.4. 代…...

Qt中的 tableView 设置 二进制 十六进制 序号表头

二 进制序号 因为QTableView的垂直表头并不支持使用委托来自定义。 相反&#xff0c;可以通过将自定义的QWidget作为QHeaderView的标签来实现这一目标。 代码&#xff1a; #include <QApplication> #include <QMainWindow> #include <QVBoxLayout> #include …...

leetCode121.买卖股票的最佳时机

题目&#xff1a; 给定一个数组prices,它的第i个元素prices[i]表示一支给定股票第i天的价格。 你只能选择某一天买入这只股票&#xff0c;并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔交易中获取的最大利润。如果你…...

基于单片机的自限位电机正反转控制电路

【摘要】 针对减速直流电机正反转和停转控制问题,设计了正反转及自限位控制电路,编制了基于STM8S103单片机的控制电机正反转及自限位的子程序,并应用IAR软件进行仿真,搭建实验装置验证设计效果,实验结果表明所设计控制电路能够良好地控制电机正转、反转和停转,在生活实际…...

为什么使用 Token 而不是 Cookie 来管理用户认证和会话

使用 Token 而不是 Cookie 来管理用户认证和会话的原因主要有以下几点&#xff1a; 跨域问题 (Cross-Domain) Token&#xff1a;通常是存储在客户端的 localStorage 或 sessionStorage 中&#xff0c;不会自动随请求发送到服务器&#xff0c;这使得它在处理跨域请求时更加灵活…...

WIDER FACE数据集转YOLO格式

1. 引出问题 本人最近在做毕设相关内容&#xff0c;第一阶段目标是通过目标检测来统计课堂人数&#xff0c;因此需要对人脸和人头进行目标检测。模型方面没什么好说的无脑用YOLO&#xff0c;数据集方面&#xff0c;人脸部分找到了来自港中文的WIDER FACE数据集。但是解压后发现…...

机器学习概述详解

文章目录 机器学习概述详解一、引言二、机器学习基础1、机器学习定义及应用场景2、监督学习与无监督学习 三、机器学习开发流程四、使用示例1、LeNet网络结构代码示例2、AlexNet网络结构代码示例 五、总结 机器学习概述详解 一、引言 机器学习作为人工智能的一个重要分支&…...

VTK编程指南<三>:基于VTK入门程序解析来理解VTK基础知识

1、VTK入门程序 下面是一个完整的Vtk入门程序&#xff0c;我们基于这个程序来对VTK的基本知识进行一个初步了解。 #include <iostream>#include <vtkAutoInit.h> VTK_MODULE_INIT(vtkRenderingOpenGL2);// VTK was built with vtkRenderingOpenGL2 VTK_MODULE_INI…...

【UE5 C++课程系列笔记】07——使用定时器实现倒计时效果

使用定时器实现如下倒计时效果 效果 步骤 1. 新建一个Actor类&#xff0c;这里命名为“CountDownTimerActor” 2. 在头文件中先定义倒计时时间和更新剩余时间的函数方法 前向声明一个文本渲染组件 3. 在源文件中引入文本渲染组件 创建文本渲染组件并进行一些设置 实现Update…...

基于DDPM的PyTorch简单实现

基于DDPM的PyTorch简单实现 文章目录 基于DDPM的PyTorch简单实现摘要Abstract一、DDPM实现1. 获取数据集2. DDPM类3. 训练算法4. 去噪神经网络5. 实验结果与采样 总结 摘要 本周的学习与实践围绕扩散模型&#xff08;Diffusion Model&#xff09;的基础理论和实现展开&#xf…...

php laravel 学习管理系统(LMS)

Lernen LMS&#xff08;学习管理系统&#xff09;是一个综合性的在线教育平台&#xff0c;旨在为学生和导师提供灵活、高效、便捷的学习体验。该系统不仅帮助学生找到最适合自己的导师&#xff0c;还通过一系列强大的功能&#xff0c;提升了课程安排、学习管理和师生互动的效率…...

【JAVA】Java高级:数据库监控与调优:SQL调优与执行计划的分析

作为Java开发工程师&#xff0c;理解SQL调优和执行计划的分析是至关重要的。这不仅可以帮助我们提高数据库查询的效率&#xff0c;还能减少系统资源的消耗&#xff0c;提升整体应用的性能。 1. SQL调优的重要性 随着数据量的增加和用户请求的增多&#xff0c;数据库的性能问题…...

centos9升级OpenSSH

需求 Centos9系统升级OpenSSH和OpenSSL OpenSSH升级为openssh-9.8p1 OpenSSL默认为OpenSSL-3.2.2&#xff08;根据需求进行升级&#xff09; 将源码包编译为rpm包 查看OpenSSH和OpenSSL版本 ssh -V下载源码包并上传到服务器 openssh最新版本下载地址 wget https://cdn.openb…...

jeccg-boot修改密码

最近在使用jeccg-boot框架&#xff0c;遇到一个需要批量修改用户密码的问题 由于框架使用的是加密盐算法生成的密码 &#xff0c;无法直接通过数据库修改密码 例如将password字段和salt值复制过去&#xff0c;密码是不对的 查看代码发现通过user.getUsername(), user.getPasswo…...

linux 生成 nginx 的https ssl 证书详解

证书生成 1. 生成证书 会提示输入密码&#xff0c;输入两次相同密码即可。 openssl genrsa -des3 -out server.key 20482. 去除密码校验 如果想去除此输密码的步骤&#xff0c;可以执行如下命令&#xff0c;根据使用需求选择。 openssl rsa -in server.key -out server.ke…...

详细介绍vue的递归组件(重要)

递归组件在 Vue 中是一个非常强大的概念&#xff0c;尤其在渲染层级结构&#xff08;如树形结构、嵌套列表、评论系统等&#xff09;时&#xff0c;能够极大地简化代码。 什么是递归组件&#xff1f; 递归组件就是一个组件在其模板中引用自身。这种做法通常用于渲染树形结构或…...

gitlab配置调试minio

官方文档 rails console 调试 查看配置Settings.uploads.object_store加载minio clientrequire fog/awsfog_connection Fog::Storage.new(provider: AWS,aws_access_key_id: 你的MINIO_ACCESS_KEY,aws_secret_access_key: 你的MINIO_SECRET_KEY,region: <S3 region>,e…...

Docker(Nginx) 部署 uniapp

目录 一、准备工作 1.Docker安装nginx 2.安装HBuild X工具 二、HBuild X打包项目 1.在HBuild X导入项目 2.配置manifest.json 3.打包 &#xff08;1&#xff09;点击发行 &#xff08;2&#xff09;填写信息&#xff0c;点击发行 三、nginx部署uniapp 1.生成文件上传…...

Camp4-L2:LMDeploy 量化部署进阶实践

书生浦语大模型实战营第四期&#xff1a;LMDeploy 量化部署进阶实践 教程链接&#xff1a;https://github.com/InternLM/Tutorial/tree/camp4/docs/L2/LMDeploy视频链接&#xff1a;https://www.bilibili.com/video/BV18aUHY3EEG/?vd_sourceb96c7e6e6d1a48e73edafa36a36f1697…...

第二十四周学习周报

目录 摘要Abstract1. 文献阅读1.1 RNN1.2 Deep Recurrent Neural Networks1.3 实验1.4 讨论 2. AI虚拟主播生成总结 摘要 本周的主要任务是阅读了一篇关于循环神经网络的论文&#xff0c;该论文旨在探索将RNN扩展到深度RNN的不同方法。论文通过对RNN结构的理解和分析&#xff…...

深入解析 Android PMS —— APK 安装与解析全流程

文章目录 前言1. PMS 的初始化1.1 SystemServer 启动 PMS1.2 PMS 的入口方法 main1.3 PMS 构造函数1.4 扫描 APK 文件1.5 权限初始化1.6 提供对外服务 2. APK 安装机制2.1. 安装请求的触发2.2 APK 文件解析与验证2.3 签名校验2.4 权限管理2.4.1 权限声明2.4.2 权限校验与分配 2…...

RL仿真库pybullet

1. 介绍 PyBullet是一个基于Bullet Physics引擎的物理仿真Python接口&#xff0c;主要用于机器人仿真模拟。 1.1 主要特点 提供大量预设的机器人模型&#xff0c;例如URDF(统一机器人描述格式)、SDF、MJCF 格式。适用于训练和评估强化学习算法&#xff0c;提供了大量的强化学…...

Vue3组件通信(父传子,子传父,跨组件通信)

本文主要是讲述Vue3在setup语法糖下的组件间通信 Vue组件通信是指在Vue.js中&#xff0c;不同组件之间进行信息交流和共享数据的过程。在前端开发中&#xff0c;组件通信是非常重要的一部分&#xff0c;因为在一个复杂的应用中&#xff0c;不同的组件通常需要相互协作&#xff…...

从失败中学习:如何将错误转化为学习机会

失败是人生的一部分&#xff0c;无论是在个人生活还是职业生涯中&#xff0c;我们都难免会遇到挫折和错误。然而&#xff0c;失败并不意味着终结&#xff0c;而是一个潜在的学习机会。通过正确的态度和方法&#xff0c;我们可以从失败中汲取经验&#xff0c;转化为成长的动力。…...

[0629].第29节:配置中心业务规则与动态刷新

我的后端学习大纲 SpringCloud学习大纲 1、编码实现3377服务&#xff1a; 1.1.建module: 1.2.改pom: 1.3.写YML&#xff1a; 1.Nacos同Consul一样&#xff0c;在项目初始化时&#xff0c;要保证先从配置中心进行配置拉取&#xff0c;拉取配置之后&#xff0c;才能保证项目的正…...

JUC并发编程

进程&#xff1a;系统中正在运行的一个应用程序&#xff0c;程序一旦运行就是进程&#xff0c;是资源分配的最小单元。 线程&#xff1a;系统分配处理器时间资源的基本单元&#xff0c;进程之内独立执行的一个单元执行流&#xff0c;是程序执行的最小单位。 Lock 需要手动上…...

贪心算法解题方法介绍+实操案例——会场安排与月饼售卖问题解析

从贪心算法到实操案例——会场安排与月饼售卖问题解析 前言 贪心算法是一种通过选择局部最优解来尝试构建全局最优解的算法。它简单高效&#xff0c;适用于许多优化问题。本文将详细介绍贪心算法的一般解题步骤&#xff0c;并通过两个实例——月饼售卖问题和会场安排问题——…...

ASP.NET Core API 前后端分离跨域

环境准备 数据库&#xff1a; sqlserver 2022 后端&#xff1a; vs2022 ASP.NET Core API .net 8 前端&#xff1a; Hbuilderx bootstrap 5.3.0 jquery v3.7.1 bootstrap-table 1.23.5 完整项目代码下载地址 功能 实现 单张表 的 增 删 改 查 创建数据库和表 create data…...

用Python绘制医学热图

在医学研究和临床实践中&#xff0c;数据的可视化是不可或缺的一部分。通过直观的数据展示&#xff0c;医学专业人员可以更好地理解各种疾病的治愈率、治疗效果以及医院之间的差异。今天&#xff0c;我们将介绍一种强大的数据可视化工具——热图&#xff08;Heatmap&#xff09…...

使用 Spring Boot 和 GraalVM 的原生镜像

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;历代文学&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编程&#xff0c;高并发设计&#xf…...

Flutter解压文件并解析数据

Flutter解压文件并解析数据 前言 在 Flutter 开发中&#xff0c;我们经常需要处理文件的读取和解压。 这在处理应用数据更新、安装包、存档文件等场景中尤为常见。 本文将介绍如何在Flutter中使用archive插件来解压文件并解析数据。 准备 在开始之前&#xff0c;我们需要…...

深入理解 JavaScript 引擎与消息队列的底层原理

深入理解 JavaScript 引擎与消息队列的底层原理 JavaScript 是现代 Web 开发中最为重要的编程语言之一&#xff0c;它的运行和执行方式常常是开发者关注的重点。为了更好地理解 JavaScript 的执行过程&#xff0c;我们需要深入探索 JavaScript 引擎的工作原理&#xff0c;尤其…...

使用 ANSYS Forming 和 LS-DYNA 进行金属成形仿真简介

了解金属成型 金属成型是制造业中的关键过程&#xff0c;其中原材料通过变形转化为所需的形状。这可能包括冲压、弯曲和深拉等操作。这些工艺的质量和效率在很大程度上取决于对各种参数的精确控制&#xff0c;例如材料特性、工具几何形状和加工条件。为了优化这些参数并确保成功…...

001-mysql安装

[rootcentos701 ~]# hostname -I 10.0.0.200 172.17.0.1 [rootcentos701 ~]# hostname centos701 [rootcentos701 ~]# rpm -qa | grep mariadb [rootcentos701 ~]# rpm -e --nodeps mariadb-libs-5.5.65-1.el7.x86_64 [rootcentos701 ~]# useradd mysql -s /sbin/nologin #创建…...

以攻击者的视角进行软件安全防护

1. 前言 孙子曰&#xff1a;知彼知己者&#xff0c;百战不殆&#xff1b;不知彼而知己&#xff0c;一胜一负&#xff0c;不知彼&#xff0c;不知己&#xff0c;每战必殆。 摘自《 孙子兵法谋攻篇 》在2500 年前的那个波澜壮阔的春秋战国时代&#xff0c;孙子兵法的这段话&…...

Go 语言性能优化全解析

在当今的软件开发环境中&#xff0c;Go 语言&#xff08;Golang&#xff09;因其简洁的语法、高效的并发模型和快速的编译速度而备受青睐。然而&#xff0c;随着应用程序复杂性的增加&#xff0c;即使是在 Go 中也可能会遇到性能瓶颈。为了帮助开发者构建高性能的应用程序&…...

《智能体雏形开发(高阶实操)》二、智能体雏形开发

基于阿里云百炼平台开发智能体应用:生成日报与周报 在智能体开发中,生成结构化的日报与周报是一个典型的任务。本篇文章将基于阿里云百炼平台,结合 Python 开发环境,介绍如何开发一个从日志文件提取信息并生成摘要的智能体。我们将从需求分析、任务设计到核心功能实现逐步…...

【k8s】kubelet 和 API Server的关系

文章目录 概述1. # kubelet 和 API Server 之间的关系**1. 角色和功能****1.1 kubelet****1.2 API Server** **2. 交互关系****2.1 kubelet 从 API Server 获取指令****2.2 kubelet 向 API Server 上报状态****2.3 kubelet 与 API Server 的认证和授权** **3. 典型交互场景****…...

POSTGRESQL跟ORACLE语法区别和相同之处

跟ORACLE语法区别之处 1. Update和delete语法区别 Pg 和MySQL Update和delete的时候表名不能加别名 2. 插入数字类型不一样 ORACLE 对number类型的数据可以用’’ 字符串标记插入&#xff0c;但是PG不行&#xff0c;必须要进行正确的数据类型 3. SEQ使用不同 ORACEL的SEQ…...

Distance in Tree 树形dp练习(树中两点距离为k的数量板子)

Distance in Tree 题面翻译 题目大意 输入点数为 N N N一棵树 求树上长度恰好为 K K K的路径个数 输入格式 第一行两个数字 N , K N,K N,K,如题意 接下来的 N − 1 N-1 N−1行中,每行两个整数 u , v u,v u,v表示一条树边 ( u , v ) (u,v) (u,v) 输出格式 一个整数 a n…...

【MySQL】库的操作+表的操作

库的操作表的操作 1.库的操作 1.1创建数据库1.2删除数据库1.3查找数据库1.4修改数据库1.5数据库备份和恢复1.6查看连接情况 2.库的操作 2.1创建表2.2查看表结构2.3修改表2.4删除表 点赞???收藏???关注??? 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧???…...