Skip to content

第11章 后端编译与优化

11.1 概述

在Java技术体系里面,Java程序从编写到最终执行,需要经历两个主要的编译阶段:

  1. 前端编译:由Javac编译器将Java源代码(.java文件)编译成字节码(.class文件)。这个过程发生在程序运行之前,因此也称为"早期编译"。

  2. 后端编译:由Java虚拟机中的即时编译器(Just In Time Compiler,JIT编译器)将字节码编译成本地机器码。这个过程发生在程序运行期间,因此也称为"晚期编译"或"运行期优化"。

前端编译在上一章已经详细介绍过。本章将重点讨论后端编译,包括即时编译器(JIT)的工作原理、提前编译器(AOT)的技术特点,以及编译器在运行期间采用的各种优化技术。

Java虚拟机设计团队选择把对性能的优化主要集中到后端编译上,这样做的好处在于:

  • 平台无关性:字节码作为中间表示,可以在任何支持Java虚拟机的平台上运行
  • 动态优化:运行期编译器可以收集程序运行时的统计信息,进行针对性的优化
  • 动态扩展:支持运行时代码生成和动态代理等高级特性

11.2 即时编译器

11.2.1 解释器与编译器

Java程序最初是通过解释器(Interpreter)来执行字节码的。解释器逐条将字节码解释为本地机器码并执行,这种方式启动速度快,但执行效率相对较低。

为了提高执行效率,现代Java虚拟机(如HotSpot)引入了即时编译器(JIT Compiler)。当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为"热点代码"(Hot Spot Code),然后将它们编译成本地机器码,并进行各种优化。

HotSpot虚拟机内置了两个(或三个)即时编译器:

编译器名称特点适用场景
C1编译器Client Compiler编译速度快,优化程度较低客户端模式,注重启动速度
C2编译器Server Compiler编译速度慢,优化程度高服务端模式,注重峰值性能
Graal编译器Graal Compiler使用Java编写,模块化程度高Java 10+ 实验性替代C2

解释器与编译器的协作模式

HotSpot虚拟机采用解释器与编译器并存的架构,这种混合模式(Mixed Mode)结合了两者的优势:

  1. 分层编译(Tiered Compilation)

    • 第0层:程序纯解释执行,解释器不开启性能监控功能
    • 第1层:使用C1编译器,进行简单可靠的优化,不开启性能监控
    • 第2层:使用C1编译器,开启部分性能监控功能
    • 第3层:使用C1编译器,开启全部性能监控功能
    • 第4层:使用C2编译器,进行更激进的优化
  2. 协作优势

    • 程序启动初期,使用解释器快速执行,同时收集性能数据
    • 随着热点代码的发现,逐步使用C1、C2编译器进行优化编译
    • 编译后的机器码缓存起来,后续直接执行,避免重复编译

相关JVM参数

参数说明默认值
-Xint强制使用解释模式关闭
-Xcomp强制使用编译模式关闭
-Xmixed混合模式(默认)开启
-client使用C1编译器根据系统自动选择
-server使用C2编译器根据系统自动选择
-XX:+TieredCompilation开启分层编译JDK 8+默认开启
-XX:TieredStopAtLevel=N设置分层编译停止层级4

执行模式对比示例

bash
# 纯解释执行
java -Xint -version

# 纯编译执行(所有方法首次调用即编译)
java -Xcomp -version

# 混合模式(默认)
java -Xmixed -version

# 仅使用C1编译(分层编译第1层)
java -XX:+TieredCompilation -XX:TieredStopAtLevel=1 MyApp

# 使用C1+C2但不使用分层编译(旧模式)
java -XX:-TieredCompilation MyApp

分层编译的执行流程

程序启动

解释执行(第0层)+ 收集性能数据

方法调用次数达到阈值

C1编译(第1-3层)→ 生成带性能监控的机器码

热点代码识别 + 收集更多统计数据

C2编译(第4层)→ 生成高度优化的机器码

后续调用直接执行编译后的机器码

11.2.2 编译对象与触发条件

编译对象

即时编译器的编译对象主要是"热点代码",包括两类:

  1. 被多次调用的方法:这是最常见的热点代码形式。当方法调用次数达到一定阈值时,整个方法会被编译。

  2. 被多次执行的循环体:对于循环体这种热点代码,虽然它属于某个方法,但编译时依然会以整个方法作为编译对象,这种编译方式称为栈上替换(On Stack Replacement,OSR)。

触发条件

热点代码的探测主要有两种方式:

  1. 基于采样的热点探测(Sample Based Hot Spot Detection)

    • 周期性检查各个线程的栈顶方法
    • 如果发现某个方法经常出现在栈顶,就认为是热点方法
    • 优点:简单高效,容易获取方法调用关系
    • 缺点:难以精确确定方法热度,容易受线程阻塞影响
  2. 基于计数器的热点探测(Counter Based Hot Spot Detection)

    • 为每个方法(或代码块)建立计数器,统计执行次数
    • 当计数器超过阈值时,触发即时编译
    • 优点:精确统计,不受线程调度影响
    • 缺点:需要维护计数器,有一定开销

HotSpot虚拟机采用基于计数器的热点探测,使用两种计数器:

  • 方法调用计数器(Invocation Counter):统计方法被调用的次数

    • 默认阈值:Client模式1500次,Server模式10000次
    • 可通过-XX:CompileThreshold参数调整
    • 计数器热度会随时间衰减(半衰期)
  • 回边计数器(Back Edge Counter):统计循环体执行次数

    • 用于触发OSR编译
    • 阈值计算公式:CompileThreshold * OnStackReplacePercentage / 100
    • Client模式默认:13995次
    • Server模式默认:10700次

计数器衰减机制

方法调用计数器存在热度衰减机制:当超过一定的时间限度(半衰周期),如果方法的调用次数仍然不足以触发即时编译,那该方法的调用计数器就会减少一半。这个过程称为方法调用计数器热度的衰减。

时间轴 →
方法调用计数: 0 → 5000 → 8000 → 4000(衰减) → 7000 → 10000(触发编译)
             ↑         ↑         ↑              ↑
           启动      5秒后    10秒后(半衰期)   继续调用

热点代码探测参数汇总

参数说明默认值
-XX:CompileThreshold方法调用计数器阈值10000 (Server) / 1500 (Client)
-XX:OnStackReplacePercentageOSR编译触发百分比140 (Client) / 107 (Server)
-XX:CounterHalfLifeTime计数器半衰期(秒)30
-XX:-UseCounterDecay关闭计数器衰减开启
-XX:CompileCommand=compileonly,*::method仅编译指定方法-

热点代码探测示例

java
public class HotSpotTest {
    private static int value = 0;
    
    // 热点方法 - 会被JIT编译
    public static void hotMethod() {
        for (int i = 0; i < 100000; i++) {
            value += i;
        }
    }
    
    // 普通方法 - 可能不会被编译
    public static void normalMethod() {
        System.out.println("Normal method");
    }
    
    public static void main(String[] args) {
        // 调用1000次 - 可能触发C1编译
        for (int i = 0; i < 1000; i++) {
            hotMethod();
        }
        
        // 继续调用10000次 - 可能触发C2编译
        for (int i = 0; i < 10000; i++) {
            hotMethod();
        }
    }
}

11.2.3 编译过程

Client Compiler(C1)编译过程

C1编译器是一个简单快速的三段式编译器,主要关注局部性优化,编译过程包括:

  1. 高级中间代码(High-Level Intermediate Representation,HIR)生成

    • 将字节码构造成一种与平台无关的高级中间表示
    • 使用静态单赋值(SSA)形式,便于优化
    • 进行方法内联、常量传播等基础优化
  2. 低级中间代码(Low-Level Intermediate Representation,LIR)生成

    • 将HIR转换为与目标机器相关的低级中间表示
    • 进行空值检查消除、范围检查消除等优化
    • 进行寄存器分配
  3. 机器代码生成

    • 将LIR转换为本地机器码
    • 进行简单的窥孔优化

C1编译器编译速度快,但优化程度有限,适合对启动时间敏感的客户端应用。

Server Compiler(C2)编译过程

C2编译器是专门面向服务端应用的高性能编译器,采用了大量的经典编译优化技术:

  1. 理想图(Ideal Graph)构建

    • 基于Sea-of-Nodes中间表示
    • 将控制流和数据流统一表示
    • 便于进行全局优化
  2. 全局优化

    • 方法内联(Method Inlining)
    • 逃逸分析(Escape Analysis)
    • 循环优化(Loop Optimization)
    • 全局代码外提(Global Code Motion)
  3. 寄存器分配与代码生成

    • 使用图着色算法进行全局寄存器分配
    • 生成高质量的本地机器码

C2编译器编译耗时较长,但生成的代码执行效率极高,适合长时间运行的服务端应用。

C1与C2编译器对比

特性C1 (Client Compiler)C2 (Server Compiler)
编译速度快(毫秒级)慢(秒级)
优化程度基础优化激进优化
启动时间
峰值性能中等极高
适用场景客户端、GUI应用服务端、长时间运行
中间表示HIR → LIRSea-of-Nodes (理想图)
寄存器分配线性扫描图着色
方法内联有限制(35字节)更激进(325字节)
逃逸分析不支持支持
循环优化基础高级(向量化等)

编译器选择建议

bash
# 32位系统或客户端应用(默认C1)
java -client MyApp

# 64位服务端应用(默认C2+分层编译)
java -server MyApp

# 禁用C2,仅使用C1(快速启动,较低性能)
java -XX:+TieredCompilation -XX:TieredStopAtLevel=1 MyApp

# 禁用分层编译,使用传统C2(较慢启动,较高性能)
java -XX:-TieredCompilation -server MyApp

11.2.4 实战:查看及分析即时编译结果

查看即时编译状态

使用-XX:+PrintCompilation参数可以查看方法的即时编译情况:

bash
java -XX:+PrintCompilation MyApplication

输出格式说明:

  timestamp compilation_id attributes tiered_level method_name

例如:

    123   45 %     3       java.lang.String::indexOf @ 12 (70 bytes)
  • 123:虚拟机启动后的毫秒数
  • 45:编译ID
  • %:表示OSR编译
  • 3:分层编译层级
  • java.lang.String::indexOf:被编译的方法

使用JITWatch分析

JITWatch是一个开源的JIT编译日志分析工具,可以可视化地展示:

  • 哪些方法被编译了
  • 编译耗时
  • 内联决策
  • 优化失败原因

使用方法:

  1. 添加-XX:+LogCompilation参数生成编译日志
  2. 使用JITWatch打开日志文件进行分析

查看编译后的机器码

使用-XX:+PrintAssembly参数可以查看编译后的汇编代码(需要安装hsdis插件):

bash
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly MyApplication

常用JIT诊断参数汇总

参数说明
-XX:+PrintCompilation打印方法编译信息
-XX:+LogCompilation记录详细编译日志到hotspot.log
-XX:+PrintInlining打印方法内联决策
-XX:+PrintAssembly打印编译后的汇编代码(需hsdis)
-XX:+PrintOptoAssembly打印C2编译器的优化汇编
-XX:+PrintCFGToFile将控制流图输出到文件
-XX:+PrintIdealGraphFile将理想图输出到文件
-XX:+TraceClassLoading跟踪类加载过程
-XX:+TraceClassUnloading跟踪类卸载过程

实战示例:分析热点方法编译过程

java
public class JITAnalysisDemo {
    private static int sum = 0;
    
    // 热点方法
    public static int calculate(int n) {
        int result = 0;
        for (int i = 0; i < n; i++) {
            result += i * i;
        }
        return result;
    }
    
    public static void main(String[] args) {
        // 预热
        for (int i = 0; i < 100000; i++) {
            calculate(100);
        }
        
        // 正式测试
        long start = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            sum += calculate(1000);
        }
        long end = System.nanoTime();
        
        System.out.println("Sum: " + sum);
        System.out.println("Time: " + (end - start) / 1_000_000 + " ms");
    }
}

运行并观察编译过程:

bash
# 查看编译信息
java -XX:+PrintCompilation JITAnalysisDemo

# 查看内联决策
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining JITAnalysisDemo

# 生成详细编译日志
java -XX:+LogCompilation -XX:+PrintInlining JITAnalysisDemo

PrintCompilation输出解析示例

    123   45 %     3       JITAnalysisDemo::calculate @ 4 (28 bytes)
     45   46       3       java.lang.Math::floorMod (20 bytes)
     67   47       4       JITAnalysisDemo::calculate (28 bytes)
     89   48       1       java.lang.Object::<init> (1 bytes)

输出列说明:

  • 第1列:虚拟机启动后的毫秒数
  • 第2列:编译任务ID
  • 第3列:编译属性(%=OSR编译, s=同步方法, !=有异常处理器, b=阻塞模式)
  • 第4列:分层编译层级(0-4)
  • 第5列:方法名、字节码位置、字节码大小

11.3 提前编译器

11.3.1 提前编译的优劣得失

提前编译(Ahead-of-Time Compilation,AOT)是指在程序运行之前,将字节码编译成本地机器码。JDK 9引入了Jaotc工具,支持提前编译。

提前编译的优势

  1. 启动速度快:程序启动时已经是本地代码,无需等待JIT编译
  2. 内存占用低:不需要加载JIT编译器和缓存编译后的代码
  3. 可预测的性能:没有JIT编译的预热过程,性能表现更稳定
  4. 安全性:可以提前进行代码审查和优化

提前编译的劣势

  1. 平台相关性:编译后的机器码与特定平台绑定,失去"一次编写,到处运行"的跨平台特性
  2. 无法动态优化:无法在运行时根据实际执行情况进行优化
  3. 动态特性受限:反射、动态代理、字节码生成等动态特性难以支持
  4. 代码膨胀:需要为所有可能的执行路径生成代码,可能导致代码体积增大

适用场景

  • 对启动时间要求极高的场景(如Serverless、微服务)
  • 内存资源受限的嵌入式设备
  • 需要确定性性能表现的实时系统

11.3.2 实战:Jaotc的提前编译

Jaotc基本用法

Jaotc是JDK自带的提前编译工具,可以将Java类或模块编译为共享库(.so或.dll文件)。

编译单个类:

bash
jaotc --output libHello.so Hello.class

编译整个模块:

bash
jaotc --output libjava.base.so --module java.base

使用AOT编译的库

运行程序时,通过-XX:AOTLibrary参数加载AOT编译的库:

bash
java -XX:AOTLibrary=./libHello.so Hello

AOT编译的限制

  1. 仅支持64位Linux和64位Windows系统
  2. 需要与JIT编译器配合使用,AOT编译的代码作为第一层编译
  3. 不支持某些动态特性,如动态生成的类

Jaotc常用参数

参数说明示例
--output指定输出文件名--output libHello.so
--module编译整个模块--module java.base
--class-name指定要编译的类--class-name Hello
--search-path类搜索路径--search-path ./lib
--compile-commands编译命令文件--compile-commands commands.txt
--info显示编译信息--info
--verbose详细输出--verbose

AOT编译命令文件示例

# commands.txt
compileOnly java.lang.String.*
compileOnly java.util.ArrayList.*
exclude java.lang.Object.finalize

使用命令文件进行选择性编译:

bash
jaotc --output libjava.base.so --module java.base --compile-commands commands.txt

AOT与JIT的协作

JDK采用了AOT与JIT协作的混合模式:

  • 程序启动时,优先执行AOT编译的代码
  • 运行过程中,JIT编译器继续收集性能数据
  • 对于热点代码,JIT编译器可以重新编译,生成更优化的代码
  • 最终达到接近纯JIT编译的性能水平

AOT性能对比示例

java
public class AOTDemo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        
        // 执行计算密集型任务
        long sum = 0;
        for (int i = 0; i < 1000000; i++) {
            sum += calculate(i);
        }
        
        long end = System.currentTimeMillis();
        System.out.println("Result: " + sum);
        System.out.println("Time: " + (end - start) + " ms");
    }
    
    public static long calculate(int n) {
        return n * n + n;
    }
}

性能对比:

模式启动时间峰值性能内存占用
纯JIT慢(需预热)最高较高
纯AOT最快中等最低
AOT+JIT混合接近纯JIT中等

11.4 编译器优化技术

11.4.1 优化技术概览

现代Java虚拟机的即时编译器采用了大量经典的编译优化技术,以下是一些主要的优化技术:

优化技术描述应用阶段
方法内联将方法调用替换为方法体C1/C2
逃逸分析分析对象是否逃逸出方法或线程C2
常量传播用常量值替换常量表达式C1/C2
公共子表达式消除消除重复计算C2
数组边界检查消除消除不必要的数组边界检查C2
循环优化循环展开、循环不变量外提等C2
锁消除消除不必要的同步C2
栈上分配在栈上分配对象C2
标量替换将对象拆分为标量变量C2

这些优化技术相互配合,共同提升程序的执行效率。下面重点介绍几种关键的优化技术。

优化技术效果对比

以下是一个简单的性能测试,展示不同优化技术的效果:

java
public class OptimizationDemo {
    private static final int ITERATIONS = 100_000_000;
    
    // 测试方法内联
    public static int testInlining() {
        int sum = 0;
        for (int i = 0; i < ITERATIONS; i++) {
            sum += add(i, i + 1);  // 会被内联
        }
        return sum;
    }
    
    private static int add(int a, int b) {
        return a + b;
    }
    
    // 测试逃逸分析
    public static void testEscapeAnalysis() {
        for (int i = 0; i < ITERATIONS; i++) {
            Point p = new Point(i, i + 1);  // 不会逃逸,可能栈上分配
            int sum = p.x + p.y;
        }
    }
    
    static class Point {
        int x, y;
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    
    // 测试数组边界检查消除
    public static int testBoundsCheckElimination(int[] arr) {
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {  // 边界检查可被消除
            sum += arr[i];
        }
        return sum;
    }
    
    public static void main(String[] args) {
        // 预热
        for (int i = 0; i < 10000; i++) {
            testInlining();
            testEscapeAnalysis();
            testBoundsCheckElimination(new int[100]);
        }
        
        // 正式测试
        long start, end;
        
        start = System.nanoTime();
        testInlining();
        end = System.nanoTime();
        System.out.println("Inlining test: " + (end - start) / 1_000_000 + " ms");
        
        start = System.nanoTime();
        testEscapeAnalysis();
        end = System.nanoTime();
        System.out.println("Escape Analysis test: " + (end - start) / 1_000_000 + " ms");
        
        int[] arr = new int[1000];
        start = System.nanoTime();
        testBoundsCheckElimination(arr);
        end = System.nanoTime();
        System.out.println("Bounds Check test: " + (end - start) / 1_000_000 + " ms");
    }
}

优化相关JVM参数

参数说明默认值
-XX:+DoEscapeAnalysis开启逃逸分析开启
-XX:+EliminateAllocations开启标量替换开启
-XX:+EliminateLocks开启锁消除开启
-XX:MaxInlineSize最大内联方法大小35字节
-XX:FreqInlineSize频繁调用方法的最大内联大小325字节
-XX:InlineSmallCode小代码内联阈值1000字节
-XX:+UseLoopPredicate循环优化开启
-XX:+RangeCheckElimination范围检查消除开启

11.4.2 方法内联

方法内联(Method Inlining)是最重要的优化技术之一,它为其他优化技术提供了基础。

基本原理

将目标方法的代码原封不动地"复制"到发起调用的方法中,避免真实的方法调用开销。

优化前:

java
public int add(int x, int y) {
    return x + y;
}

public int calc() {
    int a = 1;
    int b = 2;
    int c = add(a, b);  // 方法调用
    return c;
}

优化后:

java
public int calc() {
    int a = 1;
    int b = 2;
    int c = a + b;  // 直接替换为方法体
    return c;
}

优化效果

  • 消除了方法调用的开销(栈帧建立、参数传递、跳转等)
  • 扩大了优化范围,便于进行后续的常量传播、死代码消除等优化

内联条件

并非所有方法都会被内联,HotSpot虚拟机根据以下因素决定是否内联:

  1. 方法大小:方法字节码大小不能超过阈值(默认35字节,可通过-XX:MaxInlineSize调整)
  2. 调用频率:热点方法更容易被内联
  3. 方法修饰符privatefinalstatic方法更容易被内联(没有多态性)
  4. 内联深度:防止无限递归内联

多态方法的内联

对于虚方法(virtual method),由于存在多态性,编译器需要进行类型分析:

  1. 单态内联(Monomorphic Inline):如果实际只有一种类型,直接内联
  2. 双态内联(Bimorphic Inline):如果有两种类型,生成类型判断代码
  3. 多态内联(Megamorphic):如果类型过多,放弃内联

11.4.3 逃逸分析

逃逸分析(Escape Analysis)是C2编译器的一项重要优化技术,用于分析对象的作用域。

基本概念

逃逸分析判断一个对象是否"逃逸"出方法或线程:

  1. 方法逃逸:对象被其他方法访问(如作为参数传递、作为返回值返回)
  2. 线程逃逸:对象被其他线程访问(如赋值给静态变量)

如果对象没有逃逸,编译器可以进行以下优化:

1. 栈上分配(Stack Allocation)

如果对象不会逃逸出方法,可以在栈上分配内存,而不是堆上。

优点:

  • 对象随栈帧销毁而销毁,无需垃圾回收
  • 减少GC压力
  • 提高内存访问局部性

2. 标量替换(Scalar Replacement)

如果对象不会逃逸,且可以被拆分为基本类型(标量),则直接用标量替换对象。

优化前:

java
public void method() {
    Point p = new Point(1, 2);  // 创建对象
    int x = p.x;  // 访问字段
    int y = p.y;
    System.out.println(x + y);
}

优化后:

java
public void method() {
    int x = 1;  // 直接用标量替换
    int y = 2;
    System.out.println(x + y);
}

3. 同步消除(Synchronization Elimination)

如果对象不会逃逸出线程,可以消除对该对象的同步操作。

优化前:

java
public void method() {
    StringBuffer sb = new StringBuffer();
    sb.append("hello");  // synchronized方法
    sb.append(" world");
}

优化后:

java
public void method() {
    StringBuffer sb = new StringBuffer();
    sb.appendUnsynchronized("hello");  // 消除同步
    sb.appendUnsynchronized(" world");
}

开启逃逸分析

JDK 8及以后版本默认开启逃逸分析,可通过以下参数控制:

bash
-XX:+DoEscapeAnalysis    # 开启逃逸分析(默认开启)
-XX:-DoEscapeAnalysis    # 关闭逃逸分析
-XX:+PrintEscapeAnalysis # 打印逃逸分析结果(诊断模式)

11.4.4 公共子表达式消除

公共子表达式消除(Common Subexpression Elimination,CSE)是一种经典的编译优化技术。

基本原理

如果一个表达式之前已经计算过,且表达式中的变量值没有改变,则后续相同的表达式可以直接使用之前计算的结果,避免重复计算。

优化前:

java
public int calc(int x, int y) {
    int a = (x + y) * 10;  // 第一次计算x+y
    int b = (x + y) * 20;  // 第二次计算x+y
    return a + b;
}

优化后:

java
public int calc(int x, int y) {
    int t = x + y;         // 只计算一次
    int a = t * 10;
    int b = t * 20;
    return a + b;
}

数组范围检查消除

公共子表达式消除也适用于数组范围检查。如果编译器能确定数组访问不会越界,可以消除重复的范围检查。

11.4.5 数组边界检查消除

Java语言规范要求每次数组访问都要进行边界检查,这在循环中会带来较大的性能开销。

基本原理

编译器通过数据流分析,确定数组访问不会越界时,可以消除边界检查。

优化前:

java
public void loop(int[] array) {
    for (int i = 0; i < array.length; i++) {
        // 每次访问都要检查: i >= 0 && i < array.length
        array[i] = i;
    }
}

优化后:

java
public void loop(int[] array) {
    for (int i = 0; i < array.length; i++) {
        // 编译器证明不会越界,消除检查
        array[i] = i;  // 直接访问,无需检查
    }
}

消除条件

编译器在以下情况下可以消除边界检查:

  1. 循环变量范围明确小于数组长度
  2. 数组访问下标是循环变量的简单函数
  3. 通过范围分析证明不会越界

11.5 深入理解Graal编译器

11.5.1 历史背景

Graal编译器是Java生态系统中最新的编译器技术,代表了JIT编译器的未来发展方向。

发展历程

  1. Maxine项目:Graal的前身,是一个用Java编写的元循环虚拟机研究项目
  2. Graal项目:2011年启动,目标是使用Java实现高性能JIT编译器
  3. JDK 9:引入JVMCI(JVM Compiler Interface),允许在JVM中插入用Java编写的编译器
  4. JDK 10:Graal作为实验性C2替代器引入
  5. JDK 11+:Graal持续改进,成为AOT编译的基础

设计目标

  • 模块化:易于理解和修改的代码结构
  • 语言无关性:不仅支持Java,还支持其他JVM语言
  • 高性能:达到或超越C2编译器的性能
  • 可扩展性:便于实现新的优化和语言特性

Graal与C2编译器对比

特性Graal编译器C2编译器
实现语言JavaC++
代码可维护性较低
模块化程度
编译速度中等
峰值性能接近C2最高
内存占用较高中等
动态编译支持支持
提前编译(AOT)原生支持不支持
多语言支持优秀(Truffle框架)仅Java
调试便利性好(Java代码)较难(C++代码)

GraalVM版本演进

版本特性
JDK 9引入JVMCI接口
JDK 10Graal作为实验性JIT编译器
JDK 11Graal成为独立项目,支持AOT编译
JDK 17+GraalVM Native Image成熟,支持更多语言

11.5.2 构建编译调试环境

环境准备

  1. 下载GraalVM

    bash
    # 使用SDKMAN安装
    sdk install java 21.0.1-graalce
    sdk use java 21.0.1-graalce
  2. 获取Graal源码

    bash
    git clone https://github.com/oracle/graal.git
    cd graal/compiler
  3. 构建Graal

    bash
    mx build

使用Graal作为JIT编译器

bash
java -XX:+UnlockExperimentalVMOptions \
     -XX:+UseJVMCICompiler \
     -XX:+EnableJVMCI \
     MyApplication

11.5.3 JVMCI编译器接口

JVMCI(JVM Compiler Interface)是JDK 9引入的编译器接口,允许用Java编写的编译器与HotSpot虚拟机交互。

JVMCI架构

┌─────────────────────────────────────────┐
│           Java Application              │
├─────────────────────────────────────────┤
│  HotSpot JVM  │  JVMCI Compiler (Graal) │
│  - 解释器      │  - 用Java编写           │
│  - C1编译器    │  - 独立类加载器          │
│  - C2编译器    │  - 通过JVMCI接口交互     │
└─────────────────────────────────────────┘

JVMCI提供的主要功能

  1. 访问JVM内部数据结构:如类、方法、常量池等
  2. 安装编译后的代码:将编译后的机器码安装到代码缓存
  3. 性能监控数据:获取方法调用计数、分支频率等运行时信息
  4. 推测性优化支持:支持基于类层次结构的推测性优化和去优化

11.5.4 代码中间表示

Graal编译器使用**程序依赖图(Program Dependence Graph,PDG)**作为中间表示,这是一种基于图的IR。

图结构特点

  1. 节点(Node):表示操作(如加法、加载、调用等)
  2. 边(Edge):表示数据依赖和控制依赖
  3. 控制流与数据流统一:在一个图中表示所有信息

主要节点类型

节点类型描述
ConstantNode常量值
ParameterNode方法参数
LoadFieldNode字段读取
StoreFieldNode字段存储
InvokeNode方法调用
IfNode条件分支
LoopBeginNode循环开始
ReturnNode方法返回

优化示例

通过图变换进行优化,如常量折叠:

优化前:

AddNode
├── ConstantNode(2)
└── ConstantNode(3)

优化后:

ConstantNode(5)

11.5.5 代码优化与生成

Graal编译器的优化流程:

  1. 前端:字节码解析,构建高层次的图表示
  2. 中端:进行各种优化变换
  3. 后端:指令选择、寄存器分配、代码生成

主要优化阶段

  1. Canonicalization:规范化变换,如常量折叠
  2. Inlining:方法内联
  3. Escape Analysis:逃逸分析
  4. Partial Escape Analysis:部分逃逸分析(Graal特有)
  5. Loop Optimizations:循环优化
  6. Vectorization:向量化

部分逃逸分析(Partial Escape Analysis)

这是Graal对逃逸分析的改进,允许对象在部分控制流路径上分配在栈上:

java
public void method(boolean cond) {
    Object obj = new Object();  // 可能栈上分配
    if (cond) {
        global = obj;  // 此处逃逸,需要在堆上分配
    }
    // 其他路径可以栈上分配
}

Graal编译器性能测试

java
public class GraalPerformanceTest {
    private static final int ITERATIONS = 100_000_000;
    
    public static void main(String[] args) {
        // 预热
        for (int i = 0; i < 10000; i++) {
            compute();
        }
        
        // 测试性能
        long start = System.nanoTime();
        long result = 0;
        for (int i = 0; i < ITERATIONS; i++) {
            result += compute();
        }
        long end = System.nanoTime();
        
        System.out.println("Result: " + result);
        System.out.println("Time: " + (end - start) / 1_000_000 + " ms");
    }
    
    private static long compute() {
        long sum = 0;
        for (int i = 0; i < 100; i++) {
            sum += i * i;
        }
        return sum;
    }
}

运行对比:

bash
# 使用C2编译器(默认)
java GraalPerformanceTest

# 使用Graal编译器
java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler GraalPerformanceTest

# 使用GraalVM Native Image(AOT编译)
./graalvm-native-image GraalPerformanceTest
./graalperformancetest

GraalVM Native Image

GraalVM的Native Image功能可以将Java应用编译为本地可执行文件:

bash
# 安装native-image工具
gu install native-image

# 编译为本地可执行文件
native-image -cp . HelloWorld

# 运行(无需JVM)
./helloworld

优势:

  • 启动时间极快(毫秒级)
  • 内存占用极低
  • 可打包为容器镜像

限制:

  • 反射需要配置
  • 动态代理需要配置
  • JNI需要配置
  • 不支持某些动态特性

代码生成

Graal使用LIR(Low-level Intermediate Representation)作为机器无关的低层IR,最终生成目标平台的机器码。

11.6 本章小结

本章详细介绍了Java虚拟机的后端编译与优化技术:

  1. 即时编译器(JIT)

    • HotSpot虚拟机采用解释器与编译器并存的架构
    • C1编译器注重编译速度,C2编译器注重峰值性能
    • 分层编译结合了两者的优势
    • 热点代码探测基于方法调用计数器和回边计数器
  2. 提前编译器(AOT)

    • Jaotc工具支持将字节码提前编译为本地代码
    • 优势是启动快、内存占用低
    • 劣势是失去跨平台性和动态优化能力
    • 适合Serverless和微服务场景
  3. 编译优化技术

    • 方法内联是最重要的基础优化
    • 逃逸分析支持栈上分配、标量替换和同步消除
    • 公共子表达式消除避免重复计算
    • 数组边界检查消除减少运行时检查
  4. Graal编译器

    • 用Java编写的新一代JIT编译器
    • 通过JVMCI接口与HotSpot集成
    • 使用图结构的中间表示
    • 支持部分逃逸分析等高级优化

理解这些编译优化技术,有助于开发者写出更适合JIT编译器优化的代码,充分发挥Java虚拟机的性能潜力。

Released under the MIT License.