JIT: Java即时编译

702 阅读10分钟

JIT简介

在Java中,JIT编译器(即时编译器)是Java虚拟机(JVM)的一个重要组成部分。它的主要目标是提高Java程序的运行速度。Java程序的执行过程可以分为两个阶段:编译和运行。在编译阶段,Java源代码被编译成字节码(.class文件),这些字节码是平台无关的。接下来,在运行阶段,JVM会将字节码解释执行或者通过JIT编译器将字节码编译成本地代码,然后执行。

以下是Java中JIT编译器的主要特点:

  1. 按需编译:JIT编译器并不是一开始就将所有字节码编译成本地代码,而是在程序运行过程中,根据需要动态地编译热点代码(被频繁执行的代码片段)。这可以有效减少编译时间,并且只编译那些对性能影响较大的代码,提高整体执行效率。
  2. 优化技术:JIT编译器会应用多种优化技术来提高生成的本地代码的性能,例如方法内联、循环展开、死代码消除等。这些优化有时候会牺牲一定的编译时间,但能带来更高的运行速度。
  3. 适应性:JIT编译器具有适应性,能够根据程序的运行特点和硬件环境进行优化。例如,它可以收集运行时的信息(如频繁调用的方法、热点代码等),并据此做出优化决策。
  4. 平衡:通过将字节码转换为本地代码,JIT编译器在编译时间和运行时间之间寻求平衡。对于那些只执行一次或很少执行的代码,解释执行可能是更合适的选择,因为编译会增加额外的开销。而对于热点代码,JIT编译可以显著提高性能。

Untitled.png

  • JVM中会设置一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译

JIT编译器实现

在Java虚拟机(JVM)中,JIT编译器有多种实现,用于提高Java程序的运行性能。Client Compiler (C1)、C2 Compiler 和 Graal Compiler 是JIT编译器的三种主要实现。以下是它们的简要介绍:

  1. Client Compiler (C1):C1编译器也被称为“轻量级”JIT编译器,它的主要目标是在尽量减少启动时间的同时,为Java应用程序提供良好的性能。C1编译器主要专注于快速编译以及应用一些基本的优化技术。对于那些启动速度很重要的应用程序(如桌面应用或短暂运行的命令行工具),C1编译器是一个很好的选择。
  2. C2 Compiler:C2编译器也被称为“重量级”或“服务端”JIT编译器,它的主要目标是为长时间运行的、对性能要求较高的Java应用程序(如服务器端应用)提供最佳性能。相比于C1编译器,C2编译器在编译过程中会应用更多的优化技术,如循环展开、方法内联、常量传播等。这些优化技术可能会增加编译时间,但对于长时间运行的应用程序来说,这是值得的,因为它们可以显著提高运行性能。
  3. Graal Compiler:Graal编译器是一个相对较新的JIT编译器实现,它旨在提供与C2编译器相当的性能,同时具有更低的编译开销和更高的可扩展性。Graal编译器是用Java编写的,这使得它更易于维护和扩展。此外,Graal编译器还包括一个名为GraalVM的跨语言运行时,它支持多种编程语言(如JavaScript、Python、Ruby等)与Java共享同一JVM实例,从而实现跨语言互操作。

这三种JIT编译器实现各有优缺点,具体选择哪种实现取决于应用程序的需求和运行环境。通常情况下,C1编译器适用于那些对启动速度要求较高的应用程序,C2编译器适用于长时间运行且对性能要求较高的应用程序,而Graal编译器则适用于需要跨语言互操作和高度可扩展性的场景。

Client Compiler (C1),C2 Compiler

在运行Java应用程序时,你可以通过传递特定的命令行参数来指定使用Client Compiler (C1)或C2 Compiler。以下是如何启用这两种编译器的方法:

  1. 启用Client Compiler (C1):要启用C1编译器,需要使用client参数。这将使用C1编译器作为JIT编译器来运行你的Java应用程序。例如:

    java -client -jar your_app.jar
    

    请注意,-client参数主要用于那些对启动速度要求较高的应用程序,如桌面应用或短暂运行的命令行工具。C1编译器会在编译速度和程序性能之间取得平衡。

  2. 启用C2 Compiler:要启用C2编译器,需要使用server参数。这将使用C2编译器作为JIT编译器来运行你的Java应用程序。例如:

    java -server -jar your_app.jar
    

    C2编译器主要用于长时间运行且对性能要求较高的应用程序,如服务器端应用。它会应用多种优化技术以提高运行性能,但可能需要更长的编译时间。

从Java 8开始,默认的编译器策略已经发生了变化。在64位系统上,默认情况下,JVM将自动选择C2编译器。因此,在许多情况下,你可能不需要显式地指定-server参数。而对于32位系统,通常会默认使用C1编译器。

Graal Compiler

要使用Graal编译器,你需要安装GraalVM,这是一个基于OpenJDK的高性能运行时环境,它包括了Graal编译器。以下是使用Graal编译器的步骤:

  1. 下载和安装GraalVM:访问GraalVM官方网站 ↗,根据你的操作系统和需要选择合适的GraalVM版本(Community Edition或Enterprise Edition)。下载后,解压缩并按照官方文档的说明配置环境变量。

  2. 设置GraalVM为默认JVM:确保JAVA_HOME环境变量指向GraalVM的安装目录,同时将GraalVM的bin目录添加到PATH环境变量中。这样,在执行java命令时,系统将使用GraalVM作为默认的Java运行时环境。

  3. 启用Graal编译器:在运行Java应用程序时,使用XX:+UseJVMCICompiler选项来启用Graal编译器。例如:

    java -XX:+UseJVMCICompiler -jar your_app.jar
    

    这将使用Graal编译器作为JIT编译器来运行你的Java应用程序。你还可以使用其他JVMCI(Java虚拟机编译器接口)相关选项来配置Graal编译器的行为。

  4. 可选:使用Native Image生成本地可执行文件:GraalVM还提供了一个名为Native Image的工具,它可以将Java应用程序编译成本地可执行文件。这将进一步提高应用程序的启动速度和性能。要使用Native Image工具,首先安装它:

    gu install native-image
    
    

    然后使用native-image命令将你的Java应用程序编译成本地可执行文件:

    native-image -jar your_app.jar
    
    

    这将生成一个与你的操作系统相关的可执行文件,你可以直接运行它,无需依赖JVM。

虽然Graal编译器在许多情况下可以提供很好的性能,但它可能不适用于所有Java应用程序。

逃逸分析与即时编译优化

逃逸分析(Escape Analysis)是一种编译器优化技术,用于分析对象在程序运行过程中的作用域和生命周期。逃逸分析的主要目的是确定对象是否“逃逸”了它们所在的方法或线程,从而使编译器能够应用一系列优化措施以提高程序性能和降低内存开销。

许多现代JIT编译器,如Java HotSpot虚拟机的C2编译器和Graal编译器,都支持逃逸分析。

在逃逸分析中,编译器会检查对象引用的使用情况,以确定它们是否:

  1. 只在创建它们的方法内部使用(不逃逸)
  2. 传递给其他方法但仍在同一线程中使用(方法逃逸)
  3. 被多个线程共享访问(全局逃逸)

若能确定一个对象不会逃逸到方法或线程外(即其它方法、线程无法访问到该对象)或逃逸程度较低(只逃逸出方法而不逃逸出线程),则可为该对象实例采取不同程度的优化方案。

根据逃逸分析的结果,编译器可以应用以下优化措施:

  1. 栈上分配:如果一个对象仅在创建它的方法内部使用,编译器可以将其分配在栈上,而不是在Java堆上。栈上分配的优点是,当方法执行结束时,栈上的内存会自动回收,无需垃圾收集器进行额外的处理。这可以提高程序性能并减少内存开销。
  2. 消除同步:如果一个对象仅在单个线程中使用,编译器可以消除与该对象相关的同步操作,因为在单线程环境下,不存在竞争条件。消除不必要的同步操作可以减少程序的运行时开销。
  3. 标量替换:如果一个对象的字段在方法中独立使用,编译器可以将这个对象分解成独立的标量变量。这可以减少内存分配和访问开销,提高程序性能。

JITwatch

JITWatch是一个用于分析和可视化Java HotSpot虚拟机即时编译器(JIT)日志的开源工具。它可以帮助开发者理解Java程序在运行时是如何被JIT编译器优化的,以便更好地优化代码性能和诊断性能问题。

JITWatch提供了以下功能:

  1. 日志分析:JITWatch可以解析JIT编译器的日志文件,提供有关方法编译、内联和优化的详细信息。
  2. 可视化:JITWatch提供了一种基于时间轴的可视化方法,展示了方法的编译和执行情况。这有助于开发者理解程序的运行时行为。
  3. 源代码映射:JITWatch能够将JIT编译器日志中的信息映射回源代码,方便开发者查看哪些代码片段被优化或内联。
  4. 建议报告:JITWatch可以生成建议报告,为开发者提供关于如何进一步优化代码性能的建议。

要使用JITWatch,首先需要开启Java HotSpot虚拟机的JIT编译器日志。这可以通过添加以下参数来实现:

-XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:LogFile=jit_log.xml

然后,从JITWatch GitHub仓库 ↗下载并构建JITWatch。运行JITWatch时,指定JIT编译器日志文件(例如jit_log.xml)以及Java源代码的位置。JITWatch将分析日志文件并提供一个可视化界面,以帮助开发者深入了解程序的运行时性能。

请注意,JITWatch主要针对Java HotSpot虚拟机的C1和C2编译器,其他JVM实现(如GraalVM、OpenJ9等)可能不支持或只部分支持JITWatch。

基本功 | Java即时编译器原理解析及实践