前端编译与后端编译
前端编译
前端编译主要负责解析Java源代码(.java文件)并将其转换为一种中间表示形式,通常是字节码(.class文件)。这个过程主要包括词法分析、语法分析、语义分析和字节码生成等步骤。
- 词法分析:将源代码的字符流转换为一系列的标记(tokens)。
- 语法分析:根据Java的语法规则,将标记转换为抽象语法树(Abstract Syntax Tree,AST)。
- 语义分析:检查AST是否符合Java语言的语义规则,例如类型检查、变量作用域检查等。
- 字节码生成:将AST转换为Java字节码。这些字节码可以在JVM上运行。
后端编译
后端编译则主要负责优化和执行字节码。JVM在执行字节码时,会将其转换为机器码,这一过程也称为即时编译(Just-In-Time Compilation,JIT)。
JIT编译器会分析字节码的运行情况,例如哪些方法被频繁调用,哪些循环被重复执行等,然后对这些热点代码进行优化。优化后的代码将被转换为机器码并缓存起来,以便下次执行时可以直接使用。
后端编译的目的是提高代码的执行效率。通过分析和优化热点代码,JIT编译器可以生成更高效的机器码,从而提高Java程序的运行速度。
小结:
前端编译主要关注源代码到字节码的转换,而后端编译则关注字节码到机器码的优化和执行。这两个阶段共同构成了Java编译器的完整工作流程。
解释执行与编译执行
解释执行
解释执行是指JVM逐条读取字节码,并将其解释(或翻译)为对应平台上的机器码,然后立即执行。这种方式下,不需要事先将整个程序编译成机器码,因此启动速度快,但执行效率相对较低。
解释执行主要用于以下情况:
- 程序启动初期:当程序刚刚启动时,JVM通常首先采用解释执行的方式,以便快速启动程序。
- 热点代码不频繁:如果程序中的热点代码(即频繁执行的代码)较少,或者热点代码的执行频率不高,那么解释执行可能是一个更合适的选择。
编译执行
编译执行是指JVM将字节码编译成本地机器码,并缓存起来,以便后续直接执行。这种方式下,虽然启动速度可能较慢(因为需要编译过程),但执行效率较高。
编译执行主要用于以下情况:
- 热点代码:JVM通过热点探测机制,发现频繁执行的代码(热点代码),然后将其编译成本地机器码。这样,热点代码的执行效率会得到显著提高。
- 性能敏感的代码:对于性能要求较高的代码段,JVM也可能采用编译执行的方式,以提高执行效率。
JIT编译器
JVM中的JIT(Just-In-Time)编译器是实现编译执行的关键组件。JIT编译器会根据程序的运行情况,动态地将热点代码编译成本地机器码。这种编译方式既保证了程序的快速启动,又能在运行时实现高效的代码执行。
热点代码识别
JVM使用一种称为热点探测(Hot Spot Detection)的机制来识别这些热点代码。热点探测通常基于两个计数器:
- 方法调用计数器:记录每个方法被调用的次数。
- 回边计数器:记录循环回边的执行次数。
当计数器达到一定的阈值时,JIT编译器就会将这些热点代码编译成本地机器码,以便提高执行效率。
热点代码识别主要基于两个方面的考虑:方法调用频率和循环回边执行频率。
- 方法调用频率 :
JVM会跟踪每个方法的调用次数。如果一个方法被频繁调用,它就会被认为是热点方法。这种方法的调用次数通常通过一个计数器来统计。当计数器达到一定的阈值时,该方法就会被标记为热点方法,并可能被JIT编译器编译成本地代码。 - 循环回边执行频率 :
除了方法调用,循环内部的代码也是性能优化的重要目标。循环中的"回边"(loop back edge)是指循环体结束后跳回循环开始部分的代码位置。JVM同样会跟踪这些回边的执行次数。如果一个回边的执行次数非常高,说明该循环非常频繁,因此循环体内的代码也可能是热点代码。
然而,热点代码识别并不是完美的。有时候,JIT编译器可能会误判某些代码为热点代码,导致不必要的编译开销。另外,有些代码虽然不常执行,但执行起来非常耗时(例如,复杂的算法或数据结构操作),这些代码可能不会被识别为热点代码,因此得不到及时的优化。
为了解决这个问题,JVM提供了一些调优选项,如调整热点探测的阈值、手动指定需要编译的方法等。此外,JVM的JIT编译器也在不断进化,以更好地识别和优化热点代码。
小问
HotSpot虚拟机为什么不采用执行效率更快的编译执行,而是默认采用一种混合执行的方式?
答:编译执行需要较长的预热过程,对内存有更多的资源限制,需要额外的性能消耗来识别热点代码。
小结
JVM通过结合解释执行和编译执行两种方式,实现了Java程序的高效运行。在程序启动初期和热点代码不频繁的情况下,解释执行能够快速启动程序;而在热点代码和性能敏感的代码段上,编译执行则能够显著提高执行效率。这种灵活的执行方式使得JVM能够在不同的场景下实现最优的性能表现。
客户端编译器与服务端编译器
在JVM中,编译器扮演着非常重要的角色。JVM有两种类型的编译器:客户端编译器(Client Compiler)和服务端编译器(Server Compiler),简称为C1编译器和C2编译器。
- 客户端编译器(Client Compiler):
客户端编译器通常用于桌面应用程序和客户端应用程序,它的优化级别较低,但启动速度较快。客户端编译器的目标是快速启动和响应,因此在编译时不会进行太多的优化工作。它主要关注于生成简单、快速的字节码,以便应用程序可以迅速启动并运行。 - 服务端编译器(Server Compiler):
服务端编译器通常用于服务器应用程序,它的优化级别较高,可以生成更高效的代码。服务端编译器的目标是提高应用程序的性能和吞吐量,因此在编译时会进行更多的优化工作。它会分析代码的运行时行为和性能特征,然后生成更优化的字节码,以提高应用程序的执行效率。
后端编译优化技术
方法内联Inline
把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。可减少频繁创建栈帧的开销。
注意:
- 在编程中尽量写小方法,大方法不会内联且成为热点方法后会占用更多CodeCache
- 在内存不紧张时,调整JVM参数,减少热点阈值或增加方法体阈值,让更多方法内联。
- 尽量使用final,private,static关键字修饰方法。
逃逸分析Escape Analysis
左侧的代码中,t对象不会被外部引用,只会在方法中使用,所以不会逃逸。而右侧代码中,t对象很明显被其他方法使用,就会产生逃逸。
对象一般在堆中分配:
对象在栈上分配,进行标量替换:
对象分配总体过程:
锁消除lock elision
针对synchronized关键字,当JVM检测到一个锁的代码不存在多线程竞争时,会对这个对象的锁进行锁消除。