17611538698
webmaster@21cto.com

超越字节码:探索 JVM、JIT 和性能之间的关系

编程语言 0 63 1天前
图片

导读:在Java中,JIT 编译通过在运行时将字节码转换为本机代码来提升 Java 性能,优化执行并平衡启动速度。

在计算领域,执行用高级语言编写的程序需要将源代码编译成低级语言或原生语言。这种编译被称为“提前编译”(AOT),通常在代码构建时完成,这能够有效地减少运行时的工作量。

对于 Java 来说,AOT 会生成一个中间二进制文件,即字节码,然后在Java 虚拟机 (JVM)执行期间将其转换为本机机器码这符合 Java 的 “一次编写,随处运行”(WORA)理念, 或者简单地说,达到平台独立性 

在程序执行过程中,JVM 会识别出 那些可以优化的频繁运行的代码(称为 热点) 。这种优化由即时 (JIT) 编译器在运行时完成。非常有趣的是:HotSpot VM 的名字也由此而来。

JIT 广泛应用于 .NET、JavaScript V8 等编程语言,以及 Python 和 PHP 的某些环境中。本文将仅讨论 Java 中的即时 (JIT)。

JVM 中的 JIT


在运行时,JVM 会加载已编译的代码(即字节码),并确定每个字节码的语义以进行适当的计算。运行时解释字节码需要处理器和内存等计算资源,因此执行速度比原生应用程序慢。JIT 通过在运行时将字节码编译为原生代码来帮助优化 Java 程序的性能。生成的原生编译代码会被缓存以供后续(重复)使用。


JVM 启动期间会调用大量方法。如果立即编译所有这些方法,会显著影响启动时间。因此,为了在启动时间和长期性能之间取得平衡,我们建议在 JVM 启动时仅编译那些频繁调用的方法。至于那些不常用的方法,则会根据实际使用情况稍后编译,甚至根本不编译。


JVM 内部维护一个方法调用计数来决定方法的编译阈值。每次调用时,计数器都会递减。一旦计数器达到零,JIT 就会启动并编译该方法。


JVM 维护的另一个计数器用于循环回溯。每次执行循环时,都会根据阈值检查计数器,如果超过阈值,JIT 编译将无法进行优化。


关于JIT 编译器


JIT 编译器有以下两种类型:

  • C1:客户端编译器 →  C1编译器编译门槛低,针对应用快速启动进行了优化。
  • C2:服务器编译器→  C2 编译器的编译门槛更高。因此,编译前可用的性能分析信息更加丰富。因此,C2 编译后的代码性能得到了高度优化。此外,C2 可以准确识别出那些被确定为位于应用程序关键执行路径中的方法。 


分层编译


JIT 可以根据代码的使用情况和复杂程度,以不同的优化级别进行编译。对于更高级别的优化,虽然程序性能会更好,但编译在资源利用率(即 CPU 和内存)方面会比较昂贵。


为了充分利用 C1 和 C2 的优势,不仅将它们捆绑在一起,而且还 按照如下所述在多个级别上进行分层编译

 

分层编译级别


  • 级别 0:解释代码→ 在启动期间,所有字节码都会被翻译成本机代码。在此级别,没有进行任何优化。但是,会确定并分析最常执行的代码。这些信息将在后续级别用于优化。
  • 级别 1:简单的 C1 编译代码 → 在此级别,低复杂度的方法经过优化,JVM 认为这些方法很简单。这些方法既不会进行性能分析,也不会进一步优化。
  • 级别 2:有限的 C1 编译代码 → 在此级别,只有少数常用方法会根据可用的性能分析信息进行编译。这些方法会立即编译以进行任何早期优化,而无需等待 C2 级别。请注意,这些方法稍后可能会在更高级别进行(重新)编译,也就是说,会捕获 3 或 4 个额外的性能分析。
  • 级别 3:完整的 C1 编译代码 → 在此级别,所有重要的 热方法都经过编译,并提供完整的性能分析信息。在大多数情况下,除非编译器队列已满,否则 JIT 会直接从级别 0 的解释代码跳转到级别 3。
  • 级别 4:C2 编译代码→ 在此级别,JIT 使用所有可用的丰富性能分析信息对代码进行最大程度的优化编译。对于长期执行而言,这些编译代码最为合适。由于这是优化的巅峰,因此不会捕获进一步的性能分析信息。


有趣的是,代码可以被多次(重新)编译,以进行更高级别的优化,这取决于 JIT 的判断。

优化方法


虽然 JIT 会不断努力提升或优化性能,但在某些情况下,优化后的方法可能不再适用。此外,编译器对优化的假设也可能因方法的行为而异。对于这种情况,JIT 会暂时将优化级别恢复到之前的级别,或直接恢复到 0 级。

请注意,这些方法可以使用更新的配置文件信息再次进行优化。但是,建议监控此类切换,并相应地调整源代码,以降低频繁切换的成本。

配置格式


JIT 和分层编译默认启用。但仍可能出于某些重要原因(例如诊断 JIT 引发的错误,这种情况实际上非常罕见)而禁用它们,因此应避免禁用。

要禁用 JIT,请在 JVM 启动期间指定-Djava.compiler=NONE或作为参数。-Xint

要禁用分层编译,可以指定-XX:-TieredCompilation

对于粒度控制,即仅使用 C1 编译器,请指定-XX:TieredStopAtLevel=1

要控制从 2 到 4 的各个层级的相应阈值,请参阅如下格式(使用时将 Y 替换为层级的数字):

  • -XX:TierYCompileThreshold=0
  • -XX:TierYInvocationThreshold=100
  • -XX:TierYMinInvocationThreshold=50
  • -XX:TierYBackEdgeThreshold=1500


请注意,调整任何配置都会影响程序的性能。因此,建议在进行全面的基准测试后再进行调整。

JDK Hotspot 与 GraalVM JIT


GraalVM JIT 是另一种技术实现,其与 JDK HotSpot JIT 的核心区别如下:

  • JDK HotSpot → 标准 HotSpot JVM 采用分层 JIT 方法——包含用于快速优化的简单 C1 编译器和用于深度优化的更激进的 C2(服务器)编译器。这些编译器主要用 C/C++ 编写,经过多年的不断完善,可为一般 Java 工作负载提供稳定可靠的性能。
  • GraalVM JIT → GraalVM 是在 HotSpot 基础上构建,用 Graal 编译器取代了传统的 C2 编译器。Graal 编译器以 Java 编写,引入了高级优化,例如改进的内联、部分逃逸分析和推测优化。此外,GraalVM 的扩展不仅仅局限于 JIT 改进;它支持多语言运行时,支持 JavaScript 和 Python 等语言,并提供提前 (AOT) 编译,以在合适的场景下缩短启动时间并减少内存开销。


本质上说,HotSpot 仍然是一个久经考验且稳定的 Java 应用程序运行平台,而 GraalVM 凭借其现代 JIT 编译器和附加运行时功能突破了性能和灵活性的界限。

两者之间的选择通常取决于具体的工作负载以及应用程序的性能或互操作性要求。

结论


JIT 编译通过在运行时将字节码转换为原生代码来提升 Java 程序的性能,优化常用方法并平衡启动速度。分层编译通过基于性能分析数据逐步优化代码,进一步完善了这一过程。虽然默认的 JIT 设置效果良好,但微调配置需要仔细进行基准测试,以防止出现性能问题。

对于大多数应用程序来说,这些优化可以无缝运行,无需开发人员干预。然而,了解即时 (JIT) 编译对于高性能应用程序至关重要,因为微调编译设置会显著影响执行效率。

作者:洛逸

参考:

https://en.wikipedia.org/wiki/Just-in-time_compilation

https://docs.oracle.com/en/java/javase/21/vm/java-hotspot-virtual-machine-performance-enhancements.html

https://developers.redhat.com/articles/2023/09/29/how-we-solved-hotspot-performance-puzzle#problem_and_solution

评论