2026/3/19 19:21:32
网站建设
项目流程
莒县建设局网站,上海有什么大公司,宣城网站建设费用,网站建设的整个流程图#x1f9d1; 博主简介#xff1a;CSDN博客专家#xff0c;历代文学网#xff08;PC端可以访问#xff1a;https://literature.sinhy.com/#/literature?__c1000#xff0c;移动端可微信小程序搜索“历代文学”#xff09;总架构师#xff0c;15年工作经验#xff0c;… 博主简介CSDN博客专家历代文学网PC端可以访问https://literature.sinhy.com/#/literature?__c1000移动端可微信小程序搜索“历代文学”总架构师15年工作经验精通Java编程高并发设计Springboot和微服务熟悉LinuxESXI虚拟化以及云原生Docker和K8s热衷于探索科技的边界并将理论知识转化为实际应用。保持对新技术的好奇心乐于分享所学希望通过我的实践经历和见解启发他人的创新思维。在这里我希望能与志同道合的朋友交流探讨共同进步一起在技术的世界里不断学习成长。技术合作请加本人wx注明来自csdnforeast_sea文章目录深入理解Java的 JIT即时编译器JVM 的编译器Client CompilerServer CompilerC2 CompilerGraal CompilerJVM 的分层编译JIT 的触发JIT 的编译优化中间表达形式C1 的 HIRC2 的 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 让我想起了 RedisRedis 也是将热点数据存储在内存中以此来提升访问速度。OK解释清楚了 JIT 的原理我们来看看 JIT 的实现。JVM 的编译器JVM 中集成了两种编译器一种是 Client Compiler另外一种是 Server Compiler。Client Compiler 注重启动速度和局部的优化Server Compiler 则更加关注全局的优化性能会更好但由于会进行更多的全局分析所以启动速度会慢一些。两种编译器相辅相成互为臂膀共同把 JVM 的性能带到了一个新的高度。Client Compiler就那虚拟机中的太子 HotSpot 来说吧它就带有一个 Client Compiler被称为 C1 编译器启动速度极快。C1 通常会做这三件事①、局部简单可靠的优化比如在字节码上进行一些基础优化方法内联、常量传播等。我们来举例看一下什么是方法内联假设我们有两个简单的方法publicclassExample{publicintadd(inta,intb){returnab;}publicvoidrun(){intresultadd(5,3);System.out.println(result);}}在执行 run 方法时会调用 add 方法方法内联优化后会将 add 方法的字节码直接插入到 run 方法中这样就不用再去调用 add 方法了直接执行 run 方法就可以了。publicclassExample{publicvoidrun(){inta5;intb3;intresultab;// 这里是内联后的 add 方法体System.out.println(result);}}②、将字节码编译成 HIRHigh-level Intermediate Representation别计较它中文名叫什么我觉得与其死板的翻译不如就记住它叫 HIR一种比较接近源代码的形式。通过借助 HIR 我们可以实现冗余代码消除、死代码删除等编译优化工作我们同样通过代码来看一下。publicclassOptimizationExample{publicintcalculate(intx,inty){intaxy;intbxy;// 冗余计算returna*b;}}很明显上面的代码中b 的值是可以直接通过 a 的值计算出来的所以 b 的计算就是冗余的我们可以通过 HIR 来消除这种冗余计算。publicclassOptimizationExample{publicintcalculate(intx,inty){intaxy;returna*a;// 使用一个计算结果}}在 HIR 优化阶段编译器识别到 x y 的计算是冗余的因此它将第二次计算的结果用第一次的结果替换。③、最后将 HIR 转换成 LIRLow-level Intermediate Representation比较接近机器码了。这期间会做一些寄存器分配、窥孔优化等。寄存器分配是指在编译时将程序中的变量分配到 CPU 的寄存器上。由于寄存器的访问速度远快于内存因此合理的寄存器分配可以显著提高程序的执行效率。来看这段代码inta5;intb10;intcab;System.out.println(c);在没有寄存器优化的情况下编译器会将变量 a、b、c 分配到内存中然后在执行时再从内存中读取变量的值。有了寄存器分配优化呢R15// 将 5 赋值给寄存器 R1R210// 将 10 赋值给寄存器 R2R3R1R2// 将 R1 和 R2 的和赋值给寄存器 R3这样变量 a、b、c 就被分配到了寄存器 R1、R2、R3 上而不是内存中寄存器的访问速度远快于内存所以这样的优化可以提高程序的执行效率。窥孔优化Peephole Optimization是一种在生成机器码阶段进行的局部优化技术。编译器“窥视”一小段生成的机器码并尝试找出并替换更高效的指令序列。假设有这样一段简单的机器码MOVR1,0ADDR1,5这段代码首先将寄存器 R1 置零然后再将 5 加到 R1 上窥孔优化会将这两条指令合并成一条MOVR1,5这样仅用一条指令就完成了同样的操作显然会提高代码执行的效率。Server CompilerServer Compiler 主要关注一些编译耗时较长的全局优化甚至会还会根据程序运行的信息进行一些不可靠的激进优化。这种编译器的启动时间长适用于长时间运行的后台程序它的性能通常比 Client Compiler 高 30%以上。目前Hotspot 虚拟机中使用的 Server Compiler 有两种C2 和 Graal。C2 CompilerHotspot 中默认的 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 编译器。相比 C2Graal 有这样几种关键特性①、JVM 会在解释执行的时候收集程序运行的各种信息然后根据这些信息进行一些基于预测的激进优化比如分支预测根据程序不同分支的运行概率选择性地编译一些概率较大的分支。Graal 比 C2 更加青睐这种优化所以 Graal 的峰值性能通常要比 C2 更好。②、与 C2主要用 C 编写不同Graal 使用 Java 语言编写。这样做的好处是Graal 可以直接使用 JVM 的内存管理机制不需要像 C2 那样自己实现内存管理这样就可以避免一些内存管理上的问题。③、Graal 引入了许多现代化的编译优化技术例如更复杂的内联策略、循环优化等这些在某些情况下可以比 C2 产生更优化的代码。④、改进的逃逸分析有助于更好地进行栈上分配和锁消除从而提升性能。⑤、Graal 不仅能作为 JIT 编译器使用还支持 Ahead-of-TimeAOT编译这有助于减少 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 中分层编译将程序执行状态分为五个层次层级 0 - 解释器Interpreter这是程序最初执行的阶段代码通过解释器逐行解释执行。这一阶段的目的是尽快开始执行而不等待编译完成。层级 1 - C1 编译器带有轻量级优化C1 with Simple Optimizations在这一层级代码首次由 C1 编译器编译应用了一些基本的优化如方法内联。这一阶段的编译速度较快能迅速提供优于解释执行的性能。层级 2 - C1 编译器带有完整优化C1 with Full Optimizations此层级仍由 C1 编译器处理但应用了更多优化技术如逃逸分析。虽然这些优化需要更长的编译时间但能进一步提升运行性能。层级 3 - C1 编译器带有分析数据收集C1 with Profiling 在这个层级C1 编译器除了执行优化还收集方法执行的详细分析数据如分支频率、热点代码等。这些数据将用于 C2 编译器的后续优化。层级 4 - C2 编译器优化C2 Optimizations最终阶段由 C2 编译器处理它使用收集的分析数据进行深入优化。C2 编译器的优化更加彻底和复杂适用于长时间运行的代码能够提供最佳的运行性能。下图中列举了几种常见的编译路径1图中第 ① 条路径代表编译的一般情况热点方法从解释执行到被 3 层的 C1 编译最后被 4 层的 C2 编译。2如果方法比较小比如 getter/setter3 层的 profiling 没有收集到有价值的数据JVM 就会断定该方法对于 C1 代码和 C2 代码的执行效率相同就会执行图中第 ② 条路径。在这种情况下JVM 会在 3 层编译之后放弃进入 C2 编译直接选择用 1 层的 C1 编译运行。3在 C1 忙碌的情况下执行图中第 ③ 条路径在解释执行过程中对程序进行 profiling根据信息直接由第 4 层的 C2 编译。4C1 中的执行效率是 1 层2 层3 层第 3 层一般要比第 2 层慢 35%以上所以在 C2 忙碌的情况下执行图中第 ④ 条路径。这时方法会被 2 层的 C1 编译然后再被 3 层的 C1 编译以减少方法在 3 层的执行时间。5如果编译器做了一些比较激进的优化比如分支预测在实际运行时发现预测出错这时就会进行反优化重新进入解释执行图中第 ⑤ 条执行路径代表的就是反优化。分层编译通过在不同的阶段应用不同程度的优化既提供了较快的应用启动时间又确保了长时间运行的应用能达到峰值性能。这种动态适应的编译策略是 Java 平台持续优化性能的关键手段之一。从 JDK 8 开始JVM 默认开启分层编译。JIT 的触发JVM 根据方法的调用次数以及循环回边的执行次数来触发 JIT。循环回边是一个控制流图中的概念程序中可以简单理解为往回跳转的指令比如下面这段代码publicvoidnlp(Objectobj){intsum0;for(inti0;i200;i){sumi;}}上面这段代码经过编译生成下面的字节码。public void nlp(java.lang.Object); Code: 0: iconst_0 1: istore_1 2: iconst_0 3: istore_2 4: iload_2 5: sipush 200 8: if_icmpge 21 11: iload_1 12: iload_2 13: iadd 14: istore_1 15: iinc 2, 1 18: goto 4 21: return其中偏移量为 18 的字节码将往回跳至偏移量为 4 的字节码中。在解释执行时每当运行一次该指令JVM 便会将该方法的循环回边计数器加 1。在即时编译过程中编译器会识别循环的头部和尾部。上面这段字节码中循环体的头部和尾部分别为偏移量为 11 的字节码和偏移量为 15 的字节码。编译器将在循环体结尾增加循环回边计数器的代码来对循环进行计数。当方法的调用次数和循环回边的次数的和超过由参数-XX:CompileThreshold指定的阈值时就会触发即时编译。C1 默认值为 1500C2 默认值为 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 的编译优化即时编译器会对正在运行的程序进行一系列优化包括字节码解析过程中的分析根据编译过程中代码的一些中间形式来做局部优化根据程序依赖图进行全局优化最后才会生成机器码。中间表达形式在编译原理中通常会把编译器分为前端和后端前端编译经过词法分析、语法分析、语义分析生成中间表达形式 IRIntermediate Representation后端会对 IR 进行优化生成目标代码。Java 字节码就是一种 IR但是字节码的结构复杂也不适合做全局的分析优化。现代编译器一般采用图结构的 IR也就是所谓的静态单赋值——Static Single AssignmentSSA 是目前比较常用的一种 IR。这种 IR 的特点是每个变量只能被赋值一次而且只有当变量被赋值之后才能使用。举个例子前面也讲过这里再强调一遍{a1;a2;ba;}我们可以轻易地发现 a 1 的赋值是冗余的。传统的编译器需要借助数据流分析从后至前依次确认哪些变量的值被覆盖掉了。不过如果借助了 SSA IR编译器则可以很容易识别冗余赋值。上面代码的 SSA IR 形式的伪代码可以表示为{a_11;a_22;b_1a_2;}由于 SSA IR 中每个变量只能赋值一次所以代码中的 a 在 SSA IR 中会分成 a_1、a_2 两个变量来赋值这样编译器就可以很容易通过扫描这些变量来发现 a_1 的赋值后并没有使用由此认定该赋值是冗余的。除此之外SSA IR 对其他优化方式也有很大的帮助例如下面这个死代码删除Dead Code Elimination的例子publicvoidDeadCodeElimination{inta2;intb0if(21){a1;}else{b2;}add(a,b)}可以得到 SSA IR 伪代码a_1 2; b_1 0 if true: a_2 1; else b_2 2; add(a,b)编译器通过执行字节码可以发现 else 分支不会被执行。经过死代码删除后就可以得到代码publicvoidDeadCodeElimination{inta1;intb0;add(a,b)}我们可以将编译器的每一种优化看成一个图优化算法它接收一个 IR 图并输出经过转换后的 IR 图。编译器优化的过程就是一个个图节点的优化串联起来的。C1 的 HIR前文提到了 C1 编译器内部使用了高级中间表达形式 HIR低级中间表达形式 LIR 来进行各种优化这两种 IR 都是 SSA 形式的。HIR 是由很多基本块Basic Block组成的控制流图结构每个块包含很多 SSA 形式的指令。基本块的结构如下图所示其中predecessors 表示前驱基本块由于前驱可能是多个所以是 BlockList 结构由多个 BlockBegin 组成的可扩容数组。同样successors 表示多个后继基本块 BlockEnd。除了这两部分就是主体块里面包含程序执行的指令和一个 next 指针指向下一个执行的主体块。从字节码到 HIR 的构造最终调用的是 GraphBuilderGraphBuilder 会遍历字节码将所有代码基本块存储为一个链表结构但是这个时候的基本块只有 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,i2i3] [i3]可以看出当执行 iload_1 时操作数栈压入变量 i1执行 iload_2 时操作数栈压入变量 i2执行相乘指令 imul 时弹出栈顶两个值构造出 HIR i3 : i1 * i2生成的 i3 入栈。C1 编译器的大部分优化工作都是在 HIR 之上完成的。当优化完成之后它会将 HIR 转化为 LIRLIR 和 HIR 类似也是一种编译器内部用到的 IRHIR 通过优化消除一些中间节点就可以生成 LIR形式上更加简化。C2 的 Sea-of-Nodes IRC2 编译器中的 Ideal Graph 采用的是一种名为 Sea-of-Nodes 中间表达形式同样也是 SSA 形式。它最大的特点是去除了变量的概念直接采用值来进行运算。为了方便理解可以利用 IR 可视化工具 Ideal Graph VisualizerIGV来展示具体的 IR 图。比如下面这段代码publicstaticintfoo(intcount){intsum0;for(inti0;icount;i){sumi;}returnsum;}对应的 IR 图如下所示B0 基本块中 0 号 Start 节点是方法入口B3 中 21 号 Return 节点是方法出口。红色加粗线条为控制流蓝色线条为数据流其他颜色的线条则是特殊的控制流或数据流。被控制流所连接的是固定节点其他的则是浮动节点。依赖于这种图结构通过收集程序运行的信息JVM 可以通过 Schedule 那些浮动节点从而获得最好的编译效果。方法内联来看下面这段代码publicstaticbooleanflagtrue;publicstaticintvalue00;publicstaticintvalue11;publicstaticintfoo(intvalue){intresultbar(flag);if(result!0){returnresult;}else{returnvalue;}}publicstaticintbar(booleanflag){returnflag?value0:value1;}来看一下 bar 方法的 IR 图内联后的 IR 图内联将被调用方法的 IR 图节点复制到调用者方法的 IR 图中。在这个例子中bar 方法的 IR 图中的 0 号 Start 节点被复制到了 foo 方法的 IR 图中从而避免了方法调用的开销。逃逸分析逃逸分析是 JIT 用于优化内存管理和同步操作的重要技术。通过分析对象是否逃逸到方法或线程的外部编译器可以做出更智能的存储和同步决策。逃逸分析通常是在方法内联的基础上进行的JIT 可以根据逃逸分析的结果进行诸如锁消除、栈上分配以及标量替换的优化。下面这段代码的就是对象未逃逸的例子publicclassExample{publicstaticvoidmain(String[]args){example();}publicstaticvoidexample(){FoofoonewFoo();BarbarnewBar();bar.setFoo(foo);}}classFoo{}classBar{privateFoofoo;publicvoidsetFoo(Foofoo){this.foofoo;}}在这个例子中example方法创建了两个对象Foo和Bar。然后Bar对象通过setFoo方法引用了Foo对象。Foo 对象的逃逸情况Foo对象被创建并传递给Bar对象的setFoo方法。一旦setFoo方法被调用Foo对象的引用存储在Bar对象的实例变量foo中。但是Bar对象本身在example方法结束后就不再使用。这意味着即使Foo对象的引用被存储在另一个对象中但由于Bar对象本身也不会逃逸出example方法因此Foo对象实际上也没有逃逸。Bar 对象的逃逸情况Bar对象在example方法中被创建并使用但之后没有被传递到其他方法或返回。因此Bar对象也没有逃逸出example方法。根据逃逸分析的结果JIT 可能做出以下优化决策①、锁消除如果Foo或Bar类中有同步块使用synchronized由于对象没有逃逸编译器可以安全地消除这些锁操作。②、栈上分配由于Foo和Bar对象都没有逃逸到方法之外编译器可以选择在栈上分配这两个对象而非在堆上分配。这样可以提高内存分配的效率并减少垃圾收集器的压力。堆和栈的区别可以查看这篇内容JVM 的内存数据区我们都知道 Java 的对象是在堆上分配的而堆是对所有对象可见的。同时JVM 需要对所分配的堆内存进行管理并且在对象不再被引用时回收其所占据的内存。如果逃逸分析能够证明某些新建的对象不逃逸那么 JVM 完全可以将其分配至栈上并且在 new 语句所在的方法退出时通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来我们便无须借助垃圾收集器来处理不再被引用的对象。不过 Hotspot 并没有进行实际的栈上分配而是使用了标量替换的技术。③、标量替换Scalar Replacement标量替换是一种优化技术其中编译器将一个聚合对象分解为其各个字段。如果这个对象没有逃逸出方法那么它的各个字段可以视为独立的局部变量。这种技术允许编译器进行更细粒度的优化如更好的寄存器分配和减少不必要的内存分配。考虑这段代码publicclassExample{AllArgsConstructorstaticclassCat{intage;intweight;}publicstaticvoidexample(){CatcatnewCat(1,10);addAgeAndWeight(cat.age,cat.weight);}publicstaticvoidaddAgeAndWeight(intage,intweight){// 对年龄和体重进行一些操作}publicstaticvoidmain(String[]args){example();}}对象的使用范围在example方法中创建了一个Cat对象并将其字段age和weight传递给了addAgeAndWeight方法。Cat对象在example方法中创建且只在该方法中使用没有被传递到方法外部或赋值给外部引用。逃逸分析由于Cat对象在方法外部没有引用它没有逃逸出example方法的作用域。这意味着Cat对象是一个局部对象适合进行标量替换。标量替换的应用JVM 的 JIT 编译器会分析Cat对象的使用情况。基于逃逸分析编译器可以决定不在堆上分配Cat对象而是将其分解为两个独立的局部变量age和weight。这样原本由Cat对象占用的堆空间就被节省下来而且减少了垃圾回收的压力。优化后的执行在执行example方法时Cat对象的字段age和weight直接作为栈上的局部变量处理避免了堆分配。标量替换后的伪代码如下所示publicclassExample{publicstaticvoidexample(){intcatAge1;intcatWeight10;addAgeAndWeight(catAge,catWeight);}publicstaticvoidaddAgeAndWeight(intage,intweight){// 方法实现}}可以看到Cat对象被分解为两个局部变量catAge和catWeight并且直接作为参数传递给了addAgeAndWeight方法。窥孔优化与寄存器分配前面我们也简单分析了一下窥孔优化与寄存器分配相信大家对这两个概念都有了一定的了解这里简单总结下。窥孔优化就是将编译器所生成的中间代码中的某些组合替换为效率更高的指令组比如强度削减、常数合并等看下面这个例子就是一个强度削减的例子y1x1*3经过强度削减后得到 y1(x11)x1编译器使用移位和加法削减乘法的强度使用更高效率的指令组。寄存器分配也是一种编译的优化手段在 C2 编译器中普遍的使用。它是通过把频繁使用的变量保存在寄存器中CPU 访问寄存器的速度比内存快得多可以提升程序的运行速度。经过寄存器分配和窥孔优化之后程序就会被转换成机器码保存在 codeCache 中。小结本文主要介绍了 JIT 即时编译的原理以及编译优化的过程包括JIT 的触发条件JIT 的编译优化JIT 的编译器JIT 是 JVM 的重要组成部分它可以根据程序运行的情况对热点代码进行编译优化从而提升程序的运行效率。