JIT(即时编译)是一种编译技术,它将程序在运行时动态地进行编译,以提高程序的执行效率。JIT编译器将程序的某些部分(通常是热点代码)从解释执行转换为本地机器码,以便直接在CPU上执行,而无需再次解释执行。这种优化技术广泛应用于动态语言、虚拟机和一些解释型语言的执行环境中。

我们知道,想要把高级语言转变成计算机认识的机器语言有两种方式,分别是编译和解释,虽然Java转成机器语言的过程中有一个步骤是要编译成字节码,但是,这里的字节码并不能在机器上直接执行。

所以,JVM中内置了解释器(interpreter),在运行时对字节码进行解释翻译成机器码,然后再执行。

解释器的执行方式是一边翻译,一边执行,因此执行效率很低。为了解决这样的低效问题,HotSpot引入了JIT技术(Just-In-Time)。

当JVM发现某个方法或代码块运行时执行的特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。然后JIT会把部分“热点代码”翻译成本地机器相关的机器码,并进行优化,然后再把翻译后的机器码缓存起来,以备下次使用。

HotSpot虚拟机中内置了两个JIT编译器:Client Complier和Server Complier,分别用在客户端和服务端,目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。

当 JVM 执行代码时,它并不立即开始编译代码(因为Java默认是解释执行的)。首先,如果这段代码本身在将来只会被执行一次,那么从本质上看,编译就是在浪费精力。因为将代码翻译成 java 字节码相对于编译这段代码并执行代码来说,要快很多。第二个原因是最优化,当 JVM 执行某一方法或遍历循环的次数越多,就会更加了解代码结构,那么 JVM 在编译代码的时候就做出相应的优化。

JIT编译器在Java中的工作流程如下:

  1. 解释执行:一开始,Java程序会被解释器以字节码的形式解释执行。解释器逐行执行字节码,并将其翻译成机器指令执行。这种解释执行的方式相对较慢,但具有灵活性。

  2. 热点探测:在解释执行的过程中,JVM会收集运行时的统计信息,例如方法的调用频率、循环次数等。基于这些信息,JVM会确定哪些代码是热点代码。

  3. 即时编译:一旦某段代码被确定为热点代码,JIT编译器会将这些热点代码进行即时编译。它会将字节码转换为本地机器码,并应用各种优化算法和技术。

  4. 本地代码执行:一旦代码被即时编译器转换成本地机器码,JVM就会直接执行这些本地代码,而无需再解释执行对应的字节码。由于本地代码是直接在CPU上执行的,因此执行速度较快。

需要注意的是,JIT编译器并不是Java语言本身的一部分,而是JVM的一部分。不同的JVM实现可能会采用不同的JIT编译器,例如HotSpot虚拟机使用C2编译器进行即时编译。

JIT优化在Java中起着关键的作用,它通过动态编译和优化热点代码,提高了Java程序的执行效率和性能。这使得Java成为一种高性能的编程语言,并且在许多领域广泛应用。

热点代码

JIT会将一些热点代码编译成机器能够识别的机器码然后缓存起来,比如一些经常被调用的代码,还有for循环中的代码,那么JIT如何识别出哪些是热点代码呢??

为什么说是一些热点代码缓存起来,而不是全部呢?因为缓存是需要空间存储的,可以通过以下命令查看该缓存的大小

JVM也提供了一个参数 「-XX:ReservedCodeCacheSize」 来限制该缓存的大小,如果空间满了,JIT就无法继续编译,编译执行就会变成解释执行,程序也就会通过解释器去执行

热点探测

热点探测也就是检查出那些热点代码,然后进行编译,热点探测是基于计数器的热点探测,也就是会统计每个方法被调用的次数,当次数达到一个阈值的时候,就会被认为是热点代码

虚拟机为每个方法准备了俩种计数器,「方法调用计数器」 和 「回边计数器」,在确定JVM的运行参数之后,这二个计数器都会有各自的一个阈值,达到阈值就会出发JIT编译

方法调用计数器:用于统计方法被调用的次数,客户端模式下默认是1500次,在服务端模式下默认是10000次(默认我们都是用服务端模式),我们可以使用以下命令查看

回边计数器:用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为 「回边」,该值在服务端模式下默认是10700次,

​​​​​​​

JIT是如何优化Java性能的

方法内联

方法内联的优化是指将被调用方法的代码复制到发起调用方法中,避免发生真实的方法调用,举个例子

为什么方法内联可以优化Java性能呢?我们知道一个方法的执行在JVM内存结构中虚拟机栈对应的就是入栈,方法结束就对应着出栈,出栈和入栈都是有性能消耗的,所以少一个方法执行就减少了一次对应的出栈和入栈,性能也就能够提升 

锁消除 

在非线程安全情况下,我们都会使用线程安全的容器,举个例子,比如字符串拼接的StringBuffer和StringBuilder,StringBuffer的方法被关键字synchornized修饰,所以性能会比StringBuilder差,但是在局部方法中二者的性能确实差不多的,因为在局部方法中是单线程访问的,不存在线程安全问题,

花费的时间差不多

 

然后我们可以通过启动参数 「-XX:-EliminateLocks」 关闭锁消除,**-XX:+EliminateLocks**开启锁消除

关闭了锁消除之后,StringBuffer所话费的时间明显增加了很多,性能降低了,JIT在编译的时候发现如果使用了线程安全的容器,比如StringBUffer,但是发现程序不会存在线程并发问题,就会执行锁消除来提高程序的性能

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐