深入理解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检测到一个锁的代码不存在多线程竞争时,会对这个对象的锁进行锁消除。

相关推荐
kinlon.liu5 分钟前
零信任安全架构--持续验证
java·安全·安全架构·mfa·持续验证
王哲晓26 分钟前
Linux通过yum安装Docker
java·linux·docker
java66666888831 分钟前
如何在Java中实现高效的对象映射:Dozer与MapStruct的比较与优化
java·开发语言
Violet永存31 分钟前
源码分析:LinkedList
java·开发语言
执键行天涯32 分钟前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
Adolf_19931 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
Jarlen1 小时前
将本地离线Jar包上传到Maven远程私库上,供项目编译使用
java·maven·jar
蓑 羽1 小时前
力扣438 找到字符串中所有字母异位词 Java版本
java·算法·leetcode
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
Reese_Cool1 小时前
【C语言二级考试】循环结构设计
android·java·c语言·开发语言