深入理解JVM执行引擎

前端编译与后端编译

前端编译

前端编译主要负责解析Java源代码(.java文件)并将其转换为一种中间表示形式,通常是字节码(.class文件)。这个过程主要包括词法分析、语法分析、语义分析和字节码生成等步骤。

  1. 词法分析:将源代码的字符流转换为一系列的标记(tokens)。
  2. 语法分析:根据Java的语法规则,将标记转换为抽象语法树(Abstract Syntax Tree,AST)。
  3. 语义分析:检查AST是否符合Java语言的语义规则,例如类型检查、变量作用域检查等。
  4. 字节码生成:将AST转换为Java字节码。这些字节码可以在JVM上运行。

后端编译

后端编译则主要负责优化和执行字节码。JVM在执行字节码时,会将其转换为机器码,这一过程也称为即时编译(Just-In-Time Compilation,JIT)。

JIT编译器会分析字节码的运行情况,例如哪些方法被频繁调用,哪些循环被重复执行等,然后对这些热点代码进行优化。优化后的代码将被转换为机器码并缓存起来,以便下次执行时可以直接使用。

后端编译的目的是提高代码的执行效率。通过分析和优化热点代码,JIT编译器可以生成更高效的机器码,从而提高Java程序的运行速度。


小结:

前端编译主要关注源代码到字节码的转换,而后端编译则关注字节码到机器码的优化和执行。这两个阶段共同构成了Java编译器的完整工作流程。

解释执行与编译执行

解释执行

解释执行是指JVM逐条读取字节码,并将其解释(或翻译)为对应平台上的机器码,然后立即执行。这种方式下,不需要事先将整个程序编译成机器码,因此启动速度快,但执行效率相对较低。

解释执行主要用于以下情况:

  1. 程序启动初期:当程序刚刚启动时,JVM通常首先采用解释执行的方式,以便快速启动程序。
  2. 热点代码不频繁:如果程序中的热点代码(即频繁执行的代码)较少,或者热点代码的执行频率不高,那么解释执行可能是一个更合适的选择。

编译执行

编译执行是指JVM将字节码编译成本地机器码,并缓存起来,以便后续直接执行。这种方式下,虽然启动速度可能较慢(因为需要编译过程),但执行效率较高。

编译执行主要用于以下情况:

  1. 热点代码:JVM通过热点探测机制,发现频繁执行的代码(热点代码),然后将其编译成本地机器码。这样,热点代码的执行效率会得到显著提高。
  2. 性能敏感的代码:对于性能要求较高的代码段,JVM也可能采用编译执行的方式,以提高执行效率。

JIT编译器

JVM中的JIT(Just-In-Time)编译器是实现编译执行的关键组件。JIT编译器会根据程序的运行情况,动态地将热点代码编译成本地机器码。这种编译方式既保证了程序的快速启动,又能在运行时实现高效的代码执行。

热点代码识别

JVM使用一种称为热点探测(Hot Spot Detection)的机制来识别这些热点代码。热点探测通常基于两个计数器:

  • 方法调用计数器:记录每个方法被调用的次数。
  • 回边计数器:记录循环回边的执行次数。

当计数器达到一定的阈值时,JIT编译器就会将这些热点代码编译成本地机器码,以便提高执行效率。

热点代码识别主要基于两个方面的考虑:方法调用频率和循环回边执行频率。

  1. 方法调用频率
    JVM会跟踪每个方法的调用次数。如果一个方法被频繁调用,它就会被认为是热点方法。这种方法的调用次数通常通过一个计数器来统计。当计数器达到一定的阈值时,该方法就会被标记为热点方法,并可能被JIT编译器编译成本地代码。
  2. 循环回边执行频率
    除了方法调用,循环内部的代码也是性能优化的重要目标。循环中的"回边"(loop back edge)是指循环体结束后跳回循环开始部分的代码位置。JVM同样会跟踪这些回边的执行次数。如果一个回边的执行次数非常高,说明该循环非常频繁,因此循环体内的代码也可能是热点代码。

然而,热点代码识别并不是完美的。有时候,JIT编译器可能会误判某些代码为热点代码,导致不必要的编译开销。另外,有些代码虽然不常执行,但执行起来非常耗时(例如,复杂的算法或数据结构操作),这些代码可能不会被识别为热点代码,因此得不到及时的优化。

为了解决这个问题,JVM提供了一些调优选项,如调整热点探测的阈值、手动指定需要编译的方法等。此外,JVM的JIT编译器也在不断进化,以更好地识别和优化热点代码。

小问

HotSpot虚拟机为什么不采用执行效率更快的编译执行,而是默认采用一种混合执行的方式?

答:编译执行需要较长的预热过程,对内存有更多的资源限制,需要额外的性能消耗来识别热点代码。

小结

JVM通过结合解释执行和编译执行两种方式,实现了Java程序的高效运行。在程序启动初期和热点代码不频繁的情况下,解释执行能够快速启动程序;而在热点代码和性能敏感的代码段上,编译执行则能够显著提高执行效率。这种灵活的执行方式使得JVM能够在不同的场景下实现最优的性能表现。

客户端编译器与服务端编译器

在JVM中,编译器扮演着非常重要的角色。JVM有两种类型的编译器:客户端编译器(Client Compiler)和服务端编译器(Server Compiler),简称为C1编译器和C2编译器。

  1. 客户端编译器(Client Compiler):
    客户端编译器通常用于桌面应用程序和客户端应用程序,它的优化级别较低,但启动速度较快。客户端编译器的目标是快速启动和响应,因此在编译时不会进行太多的优化工作。它主要关注于生成简单、快速的字节码,以便应用程序可以迅速启动并运行。
  2. 服务端编译器(Server Compiler):
    服务端编译器通常用于服务器应用程序,它的优化级别较高,可以生成更高效的代码。服务端编译器的目标是提高应用程序的性能和吞吐量,因此在编译时会进行更多的优化工作。它会分析代码的运行时行为和性能特征,然后生成更优化的字节码,以提高应用程序的执行效率。

后端编译优化技术

方法内联Inline

把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。可减少频繁创建栈帧的开销。

注意:

  1. 在编程中尽量写小方法,大方法不会内联且成为热点方法后会占用更多CodeCache
  2. 在内存不紧张时,调整JVM参数,减少热点阈值或增加方法体阈值,让更多方法内联。
  3. 尽量使用final,private,static关键字修饰方法。

逃逸分析Escape Analysis

左侧的代码中,t对象不会被外部引用,只会在方法中使用,所以不会逃逸。而右侧代码中,t对象很明显被其他方法使用,就会产生逃逸。

对象一般在堆中分配

对象在栈上分配,进行标量替换

对象分配总体过程

锁消除lock elision

针对synchronized关键字,当JVM检测到一个锁的代码不存在多线程竞争时,会对这个对象的锁进行锁消除。

相关推荐
chuanauc3 分钟前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴19 分钟前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao26 分钟前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc78730 分钟前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
云泽野6 小时前
【Java|集合类】list遍历的6种方式
java·python·list
二进制person7 小时前
Java SE--方法的使用
java·开发语言·算法
小阳拱白菜8 小时前
java异常学习
java