JVM-JIT编译器

211 阅读11分钟

本文是JVM系列第5篇
Just-In-Time (JIT) 编译器是运行时环境的一个组件,通过在运行时将字节码编译为本机机器代码来提高Java™ 应用程序的性能

JIT编译器的作用和原理

JIT编译器(即时编译器)是一种能够在程序运行时将代码转换成机器语言的编译器。它的作用是将高级语言(如Java、C#等)的源代码或中间代码转化为本地机器代码,以提高程序的执行速度。

JIT编译器的原理如下:

  1. 解释器阶段:在程序执行之前,源代码或中间代码会先被解释器逐行执行。解释器通过解释每一行代码的含义,并执行相应的操作,实现程序逻辑。
  2. 编译阶段:当解释器发现某个代码块或者函数被频繁执行时,就会将其标记为“热点代码”(hot spot)。这些热点代码会被传递给JIT编译器进行编译。怎么样才会被认为是热点代码呢?JVM中会设置一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译,存入codeCache中。当下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行,以此来提升程序运行的性能。整体的执行过程大致如下图所示:

image.png

  1. 编译阶段:JIT编译器会将热点代码编译成机器码,并将其保存起来,在下次调用时可以直接执行机器码,而不再需要解释器逐行解释运行。
  2. 优化阶段:在编译过程中,JIT编译器还会进行优化,比如消除冗余的计算、栈上分配对象等,以进一步提高程序的性能。
  3. 执行阶段:当程序再次执行相同的热点代码时,JIT编译器直接使用之前编译好的机器码,减少了解释的开销,提高了执行速度。

总结来说,JIT编译器的作用是实现程序的动态编译,将热点代码转换成机器码并进行优化,从而提高程序的执行效率。它能够根据程序的实际运行情况进行编译和优化,以适应不同的运行环境和硬件条件。

JIT编译器的分类

JIT编译器通常可以分为以下几类:

  1. 基于解释器的JIT编译器:这种JIT编译器会在程序运行时,对程序代码进行解释执行,并通过动态编译将频繁执行的代码编译成本地代码,以提高程序的执行效率。
  2. 基于静态编译器的JIT编译器:这种JIT编译器会在程序运行时,对预先编译好的程序代码进行优化和重新编译,以提高程序的执行效率。
  3. 基于Tracing的JIT编译器:这种JIT编译器会通过动态分析程序的执行路径,将频繁执行的代码块编译成本地代码,从而提高程序的执行效率。
  4. 基于方法内联的JIT编译器:这种JIT编译器会将多个小的方法合并成一个大的方法,并将其编译成本地代码,以减少方法调用的开销,从而提高程序的执行效率。

需要注意的是,这些JIT编译器分类并不是互相独立的,实际上,一个JIT编译器可能会同时使用多种编译技术,以达到更好的性能表现。

JVM中集成了两种编译器,Client Compiler和Server Compiler,它们的作用也不同。Client Compiler注重启动速度和局部的优化,Server Compiler则更加关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会变慢。两种编译器有着不同的应用场景,在虚拟机中同时发挥作用。

Client Compiler

HotSpot VM带有一个Client Compiler C1编译器。这种编译器启动速度快,但是性能比较Server Compiler来说会差一些。C1会做三件事:

  • 局部简单可靠的优化,比如字节码上进行的一些基础优化,方法内联、常量传播等,放弃许多耗时较长的全局优化。
  • 将字节码构造成高级中间表示(High-level Intermediate Representation,以下称为HIR),HIR与平台无关,通常采用图结构,更适合JVM对程序进行优化。
  • 最后将HIR转换成低级中间表示(Low-level Intermediate Representation,以下称为LIR),在LIR的基础上会进行寄存器分配、窥孔优化(局部的优化方式,编译器在一个基本块或者多个基本块中,针对已经生成的代码,结合CPU自己指令的特点,通过一些认为可能带来性能提升的转换规则或者通过整体的分析,进行指令转换,来提升代码性能)等操作,最终生成机器码。

Server Compiler

Server Compiler主要关注一些编译耗时较长的全局优化,甚至会还会根据程序运行的信息进行一些不可靠的激进优化。这种编译器的启动时间长,适用于长时间运行的后台程序,它的性能通常比Client Compiler高30%以上。目前,Hotspot虚拟机中使用的Server Compiler有两种:C2和Graal。

C2 Compiler

在Hotspot VM中,默认的Server Compiler是C2编译器。

C2编译器在进行编译优化时,会使用一种控制流与数据流结合的图数据结构,称为Ideal Graph。 Ideal Graph表示当前程序的数据流向和指令间的依赖关系,依靠这种图结构,某些优化步骤(尤其是涉及浮动代码块的那些优化步骤)变得不那么复杂。

Graal Compiler

从JDK 9开始,Hotspot VM中集成了一种新的Server Compiler,Graal编译器。相比C2编译器,Graal有这样几种关键特性:

  • JVM会在解释执行的时候收集程序运行的各种信息,然后编译器会根据这些信息进行一些基于预测的激进优化,比如分支预测,根据程序不同分支的运行概率,选择性地编译一些概率较大的分支。Graal比C2更加青睐这种优化,所以Graal的峰值性能通常要比C2更好。
  • 使用Java编写,对于Java语言,尤其是新特性,比如Lambda、Stream等更加友好。
  • 更深层次的优化,比如虚函数的内联、部分逃逸分析等。

Graal编译器可以通过Java虚拟机参数-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler启用。当启用时,它将替换掉HotSpot中的C2编译器,并响应原本由C2负责的编译请求。

JIT编译器的优化技术

JIT编译器通过在程序运行时将代码编译成本地代码,以提高程序的执行效率。以下是JIT编译器常用的一些优化技术:

  1. 方法内联:将多个小的方法合并成一个大的方法。这样可以减少方法调用的开销,提高程序的执行效率。
  2. 循环展开:将循环中的代码重复执行多次,减少循环开销和分支开销。通过循环展开,可以减少循环的迭代次数,从而提高程序的执行效率。
  3. 常量折叠:将常量表达式计算出结果,替换成结果值。这样可以减少运行时计算,提高程序的执行效率。
  4. 冗余代码删除:删除重复的代码,减少代码的执行次数。这样可以减少程序的执行时间,提高程序的执行效率。
  5. 数据流分析:通过分析程序的数据流,确定变量的类型和使用方式,从而进行更准确的优化。例如,可以通过数据流分析来确定某个变量的类型,从而避免类型检查的开销。
  6. 代码生成优化:通过调整代码生成器的算法和策略,生成更优化的本地代码。例如,可以优化代码的寄存器分配,减少内存访问次数等。
  7. JIT逃逸分析:对变量的动态范围进行分析,减少对象的创建和销毁,优化内存管理。例如,可以通过逃逸分析来确定某个对象是否会被其他的方法引用,从而避免不必要的对象创建和销毁。

JIT编译器的限制和缺点

虽然JIT编译器在提高程序执行效率方面有很多优点,但是也存在一些限制和缺点:

  1. 编译时间:JIT编译器需要在程序运行时对代码进行编译,这会增加程序的启动时间和响应时间,影响用户体验。
  2. 内存占用:JIT编译器需要将编译后的代码存储在内存中,这会增加程序的内存占用,特别是在大型应用程序中,可能会导致内存占用过高。
  3. 热点代码识别:JIT编译器需要识别程序中的热点代码,并将其编译成本地代码。如果程序中的热点代码发生变化,JIT编译器可能需要重新编译代码,这会影响程序的执行效率。
  4. 并发性:JIT编译器通常是单线程的,这意味着在多核CPU上,它可能无法充分利用CPU的并发性能,从而影响程序的执行效率。
  5. 跨平台支持:JIT编译器需要针对不同的平台和操作系统进行优化,这可能会导致JIT编译器的开发和维护工作量增加。
  6. 安全性:由于JIT编译器需要在程序运行时动态生成本地代码,这可能会导致一些安全问题,例如代码注入和代码执行漏洞。

实践

编译相关的重* 要参数

  • -XX:+TieredCompilation:开启分层编译,JDK8之后默认开启
  • -XX:+CICompilerCount=N:编译线程数,设置数量后,JVM会自动分配线程数,C1:C2 = 1:2
  • -XX:TierXBackEdgeThreshold:OSR编译的阈值
  • -XX:TierXMinInvocationThreshold:开启分层编译后各层调用的阈值
  • -XX:TierXCompileThreshold:开启分层编译后的编译阈值
  • -XX:ReservedCodeCacheSize:codeCache最大大小
  • -XX:InitialCodeCacheSize:codeCache初始大小

-XX:TierXMinInvocationThreshold是开启分层编译的情况下,触发编译的阈值参数,当方法调用次数大于由参数-XX:TierXInvocationThreshold指定的阈值乘以系数,或者当方法调用次数大于由参数-XX:TierXMINInvocationThreshold指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时,便会触发X层即时编译。分层编译开启下会乘以一个系数,系数根据当前编译的方法和编译线程数确定,降低阈值可以提升编译方法数,一些常用但是不能编译的方法可以编译优化提升性能。

由于编译情况复杂,JVM也会动态调整相关的阈值来保证JVM的性能,所以不建议手动调整编译相关的参数。除非一些特定的Case,比如codeCache满了停止了编译,可以适当增加codeCache大小,或者一些非常常用的方法,未被内联到,拖累了性能,可以调整内敛层数或者内联方法的大小来解决。

通过JITwatch分析编译日志

通过增加-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining -XX:+PrintCodeCache -XX:+PrintCodeCacheOnCompilation -XX:+TraceClassLoading -XX:+LogCompilation -XX:LogFile=LogPath参数可以输出编译、内联、codeCache信息到文件。
可以使用JITwatch的工具来分析编译日志。JITwatch首页的Open Log选中日志文件,点击Start就可以开始分析日志。

参考
Java即时编译器原理解析及实践

JVM系列文章

标题介绍
JVM-JVM简介及架构概述JVM介绍 架构
JVM-类加载及类加载器类加载机制
JVM-运行时数据区JVM运行时数据区
JVM-垃圾收集器及垃圾回收算法垃圾收集器和回收算法
JVM-JIT编译器JIT编译优化
JVM-JVM性能调优JVM 参数性能调优