1. 概述

在本篇文章中,我们将深入探讨 Java 的新一代即时编译器(Just-In-Time Compiler)——Graal。

我们将了解 Graal 项目是什么,并重点介绍它的一个核心组件:高性能动态 JIT 编译器。

2. 什么是 JIT 编译器?

我们先来解释一下 JIT 编译器的作用。

当我们使用 javac 编译 Java 程序时,源代码会被编译成 JVM 字节码。字节码比源码更紧凑、更简单,但计算机的 CPU 是无法直接执行它的。

❌ 要运行 Java 程序,JVM 需要解释执行这些字节码。由于解释执行速度远慢于原生机器码,因此 JVM 会引入 JIT 编译器。

JIT 编译器会在运行时将热点字节码编译为机器码,交由 CPU 直接执行。相比 javac,JIT 编译器更加复杂,它会进行一系列优化,生成高质量的机器码。

3. 深入理解 JIT 编译器

Oracle 的 JDK 实现基于开源项目 OpenJDK,其中包含了 HotSpot 虚拟机(从 Java 1.3 开始引入)。HotSpot 中包含了两个传统的 JIT 编译器:

  • C1(客户端编译器):编译速度快,优化程度较低,适合桌面应用。
  • C2(服务端编译器):编译较慢,但生成的代码优化程度更高,适合长时间运行的服务端应用。

3.1. 分层编译(Tiered Compilation)

在现代 Java 应用中,JVM 默认使用分层编译策略:

  1. 程序启动后,字节码首先被解释执行;
  2. JVM 会监控方法调用频率,对热点方法使用 C1 编译;
  3. 如果某个方法调用次数进一步增加,JVM 会再次使用 C2 进行更深度的优化。

这种方式兼顾了启动速度和运行时性能。

3.2. 服务端编译器(C2)

C2 是 HotSpot 中最复杂的 JIT 编译器,其生成的代码性能可媲美甚至超越 C++。但它的实现语言是 C++,也带来了一些问题:

❌ 容易因内存错误导致 JVM 崩溃
❌ 代码维护成本高,难以进行功能扩展

因此,Oracle 决定开发全新的 JIT 编译器 —— Graal,以替代 C2。

4. GraalVM 项目

GraalVM 是 Oracle 的一个研究项目,主要包含两个部分:

  • 一个用 Java 编写的高性能 JIT 编译器(Graal)
  • 一个多语言虚拟机,支持 Java、JavaScript、Ruby、Python、R、C/C++ 等语言

我们重点关注其中的 JIT 编译器部分。

4.1. 用 Java 编写的 JIT 编译器

Graal 是一个用 Java 编写的 JIT 编译器,它接收 JVM 字节码并生成机器码。

相比 C++ 实现,Java 带来的优势包括:

  • 更高的安全性(无崩溃、无内存泄漏)
  • 更好的 IDE 支持与调试工具
  • 编译器可以独立于 HotSpot,甚至可以自举优化自身

Graal 使用了新的 JVM 接口 JVMCI(JVM Compiler Interface)与虚拟机通信。要启用 Graal,需要在启动时添加以下参数:

-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

这意味着你可以:

✅ 使用传统分层编译器
✅ 使用 JVMCI 版本的 Graal(Java 10+)
✅ 使用完整的 GraalVM

4.2. JVM 编译器接口(JVMCI)

JVMCI 自 JDK 9 起就已集成在 OpenJDK 中,因此你可以使用任何标准 JDK 来运行 Graal。

JVMCI 的核心作用是允许你替换默认的 JIT 编译器,而无需修改 JVM 本身

接口非常简洁,核心方法如下:

interface JVMCICompiler {
    byte[] compileMethod(byte[] bytecode);
}

在实际使用中,还需要传递更多信息,如局部变量数、栈大小、解释器收集的性能数据等。因此实际调用的是:

compileMethod(CompilationRequest request)

该方法会返回编译后的机器码。

4.3. Graal 实战演示

Graal 本身也是 Java 代码,因此它也会经历解释执行 → JIT 编译的过程。我们来看一个示例程序:

public class CountUppercase {
    static final int ITERATIONS = Math.max(Integer.getInteger("iterations", 1), 1);

    public static void main(String[] args) {
        String sentence = String.join(" ", args);
        for (int iter = 0; iter < ITERATIONS; iter++) {
            if (ITERATIONS != 1) {
                System.out.println("-- iteration " + (iter + 1) + " --");
            }
            long total = 0, start = System.currentTimeMillis(), last = start;
            for (int i = 1; i < 10_000_000; i++) {
                total += sentence
                  .chars()
                  .filter(Character::isUpperCase)
                  .count();
                if (i % 1_000_000 == 0) {
                    long now = System.currentTimeMillis();
                    System.out.printf("%d (%d ms)%n", i / 1_000_000, now - last);
                    last = now;
                }
            }
            System.out.printf("total: %d (%d ms)%n", total, System.currentTimeMillis() - start);
        }
    }
}

编译并运行:

javac CountUppercase.java
java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler CountUppercase

输出示例:

1 (1581 ms)
2 (480 ms)
3 (364 ms)
4 (231 ms)
5 (196 ms)
6 (121 ms)
7 (116 ms)
8 (116 ms)
9 (116 ms)
total: 59999994 (3436 ms)

可以看到,前几次执行耗时较长,这是 JIT 编译的“预热”阶段。

若要查看 Graal 编译统计信息,可添加参数:

-Dgraal.PrintCompilation=true

4.4. 与 C2 对比

我们再运行一次,但禁用 Graal,使用默认的 C2 编译器:

java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:-UseJVMCICompiler CountUppercase

输出示例:

1 (510 ms)
2 (375 ms)
3 (365 ms)
4 (368 ms)
5 (348 ms)
6 (370 ms)
7 (353 ms)
8 (348 ms)
9 (369 ms)
total: 59999994 (4004 ms)

对比可见:

✅ Graal 初期较慢,但优化后性能更佳
❌ C2 性能更稳定,但优化上限较低

4.5. Graal 的内部数据结构

Graal 的核心思想是将字节码转换为机器码,而中间的表示形式是一个图结构 —— 程序依赖图(Program Dependence Graph)

举个简单例子,表达式 x + y 的图结构如下:

data graph x p y

蓝色箭头表示数据流(data flow),红色箭头表示控制流(control flow)。

4.6. 实际图结构分析

我们可以使用 IdealGraphVisualiser 来查看 Graal 的真实图结构。

简单方法:

int average(int a, int b) {
    return (a + b) / 2;
}

图结构如下:

graph average

如果加入循环:

int average(int[] values) {
    int sum = 0;
    for (int n = 0; n < values.length; n++) {
        sum += values[n];
    }
    return sum / values.length;
}

图结构变得复杂:

average loop detail

这种结构被称为 sea-of-nodes(节点之海),C2 也使用类似结构。

✅ 用 Java 实现 Graal 的好处在于:图结构天然适合面向对象语言,易于维护和扩展。

4.7. AOT 模式支持

⚠️ Graal 还支持 Ahead-of-Time(AOT)编译模式(Java 10+)。

✅ AOT 编译可以在不运行程序的前提下,将所有方法一次性编译为机器码。

这是 JEP 295 提出的功能,主要用于提升启动速度,弥补 JIT 预热时间长的短板。

5. 总结

本文深入探讨了 Graal 项目中的 JIT 编译器,包括:

  • 传统 JIT 编译器(C1/C2)的工作原理
  • Graal 的架构与 JVMCI 接口
  • Graal 与 C2 的性能对比
  • Graal 内部使用的图结构(sea-of-nodes)
  • AOT 编译支持

Graal 代表了 JIT 编译器的未来方向,尤其在性能优化和可维护性方面具有显著优势。

源码可在 GitHub 获取。使用时请记得添加文中提到的 JVM 参数。


原始标题:Deep Dive Into the New Java JIT Compiler - Graal | Baeldung