JVM的即时编译JIT的介绍

目录

1、JIT

1.1、定义

1.2、设计目的

1.3、热点探测策略

1.4、热点计数器

[1.5、JIT 的优化](#1.5、JIT 的优化)

2、JIT的两个阶段

[2.1. 方法内联(Inlining)](#2.1. 方法内联(Inlining))

[2.2. 循环展开(Loop Unrolling)](#2.2. 循环展开(Loop Unrolling))

[2.3. 公共子表达式消除](#2.3. 公共子表达式消除)

[2.4. 常量折叠](#2.4. 常量折叠)

[3、验证 JIT](#3、验证 JIT)


前言

Java 程序是怎么运行的?(背景知识),我们先回顾一下 Java 程序的执行流程:

bash 复制代码
.java 源文件
   ↓ 编译(javac)
.class 字节码文件(平台无关)
   ↓ 运行(java 命令)
JVM 加载并执行

如下所示:

但 JVM 不能直接执行.class文件,它有两种方式来"跑"代码:

方式 说明
解释执行 一行行读字节码,边解释边执行(慢)
即时编译(JIT) 把热点代码编译成机器码,以后直接运行(快)

👉 JVM 不是只用一种方式,而是 混合模式(Mixed Mode):解释 + 编译 结合使用

如下所示:


1、JIT

1.1、定义

JIT 是 JVM 在程序运行时,把"慢的字节码"自动翻译成"快的机器码"的过程。

就像你听外语直播:

  • 一开始是"同声传译"(解释执行,慢)
  • 听多了发现某些句子反复出现 → 直接背下来,下次直接说中文(编译成本地代码,快)

这就是 JIT 的核心思想!

1.2、设计目的

如下所示:

项目 说明
全称 Just-In-Time Compilation(即时编译)
目的 提升 Java 程序运行速度
触发条件 方法被频繁调用(成为"热点代码")
发生在哪 程序运行时(Runtime)
输入 Java 字节码(.class)
输出 本地机器码(Native Code)
是否影响语义 否,行为完全一致
依赖技术 方法计数器、逃逸分析、内联、标量替换等

举个生活例子:做饭 vs 外卖

假设你要吃饭:

❌ 方案1:每次饿了才开始做(解释执行)

  • 每次都要洗菜、切菜、炒菜......很慢
  • 适合"偶尔吃一次"

👉 类比:解释执行每条字节码,适合不常用的代码

✅ 方案2:发现某道菜天天吃 → 直接预制半成品(JIT 编译)

  • 第一次还是现做
  • 发现你连续吃一周红烧肉 → 下次提前做好冷冻
  • 再点就直接加热上桌,超级快!

👉 类比:某个方法被频繁调用 → JIT 将其编译为本地机器码,后续直接执行

1.3、热点探测策略

JVM 主要通过以下两种策略之一来判断是否为"热点",这取决于你使用的虚拟机和配置:

策略 说明 HotSpot 默认
基于计数器的热点探测 统计方法被调用的次数或循环回边次数 ✅ 使用
基于采样的热点探测 定期采样调用栈,统计哪些方法出现最多 少见

1.4、热点计数器

1、"半衰期"机制:

为了避免"只看总次数"导致老程序一直不编译,HotSpot 引入了 热度衰减(Counter Decay) 机制。

2、工作原理:

  • 每隔一段时间(方法调用计数器满一次),JVM 会:
    • 把计数器值 减半
    • 如果此时没有达到编译阈值 → 归零

🎯 目的:

只对 近期频繁执行 的方法做 JIT,而不是"曾经火过"的方法。

📌 类比:抖音不是看你历史总播放量,而是看你最近是不是爆款。

你可以通过 JVM 参数查看和调整热点探测的行为。

参数 说明 默认值(64位Server VM)
-XX:CompileThreshold=10000 方法调用次数阈值 10000(C1) / 15000(C2)
-XX:OSRCompileThreshold=14000 循环回边次数阈值 14000
-XX:+UseCounterDecay 是否启用计数器衰减
-XX:CounterHalfLifeTime=30 半衰期时间(秒) 30 秒
-XX:+PrintCompilation 打印编译日志 否(可开启)
-XX:+UnlockDiagnosticVMOptions 解锁诊断参数 必须加才能打印详细信息

1.5、JIT 的优化

代码如下所示:

java 复制代码
public class JITDemo {

    // 这个方法会被频繁调用 → 成为"热点代码"
    public static long sum(int n) {
        long result = 0;
        for (int i = 0; i < n; i++) {
            result += i;
        }
        return result;
    }

    public static void main(String[] args) {
        // 调用很多次,触发 JIT 编译
        for (int i = 0; i < 100_000; i++) {
            sum(1000);
        }
        System.out.println("完成");
    }
}

执行过程发生了什么?

java 复制代码
第1~几千次调用:解释执行(慢)
       ↓
JVM 发现 sum() 被调用了太多次 → 标记为"热点方法"
       ↓
JIT 编译器(C1 或 C2)介入:
   - 把 sum() 的字节码 编译成 CPU 能直接运行的机器码
   - 存入"代码缓存"(Code Cache)
       ↓
之后所有调用:直接跳转到机器码执行(飞快!)

🎯 结果:越跑越快!


2、JIT的两个阶段

(C1 和 C2)HotSpot JVM 有两个 JIT 编译器:

编译器 名称 特点
C1 客户端编译器(Client Compiler) 快速编译,优化程度低,适合启动快的应用
C2 服务器编译器(Server Compiler) 慢一点,但优化狠,适合长期运行的服务

如下所示:

现代 JVM(如 Java 8+)默认使用 分层编译(Tiered Compilation)

  • 先用 C1 编译(快速上手)
  • 再用 C2 深度优化(越用越快)

继续看上面的 sum() 方法,JIT 可能会做这些事:

2.1. 方法内联(Inlining)

把小方法直接"塞进"调用处,减少函数调用开销

java 复制代码
// 原始代码
result += i;

// JIT 优化后可能变成:
%rax = %rax + %rcx   ← 直接生成汇编指令

方法调用是有成本的!

当你写:

java 复制代码
int result = calculator.add(5, 3);

JVM 需要做很多事:

  • 保存当前执行状态(栈帧)
  • 分配新栈帧
  • 参数传递
  • 跳转到 add 方法
  • 执行完再返回
  • 恢复原状态

这些操作虽然快,但频繁调用小方法时,累积起来就很浪费。

✅ 所以内联的目标是:

把短小精悍的方法"展开"到调用处,避免调用开销,还能为后续优化创造条件。

示例:

java 复制代码
public class Calculator {
    // 小方法,只是返回两数之和
    public int add(int a, int b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        int sum = 0;
        for (int i = 0; i < 1000; i++) {
            sum += calc.add(i, 1);  // 调用 1000 次
        }
        System.out.println(sum);
    }
}

如果不内联:

  • 每次循环都要调用 add() 方法
  • 创建栈帧、参数压栈、跳转、返回......重复 1000 次
  • 效率低

✅ JIT 编译后(内联发生):

JVM 发现add()是个简单方法,且被频繁调用 → 决定内联。

最终执行的代码等价于:

java 复制代码
for (int i = 0; i < 1000; i++) {
    sum += (i + 1);  // 直接把 add 的逻辑"塞进来"
}

2.2. 循环展开(Loop Unrolling)

减少循环判断和跳转次数,提高指令流水线效率。把一个循环体复制多份,每次处理多个元素。

如下所示:

java 复制代码
// 优化前
for(i=0; i<1000; i++) { ... }

// 优化后
for(i=0; i<1000; i+=4) {
    result += i;
    result += i+1;
    result += i+2;
    result += i+3;
}

2.3. 公共子表达式消除

当同一个表达式被多次计算时,JIT 编译器会识别出来,只计算一次,并将结果复用。

java 复制代码
public double compute(double a, double b) {
    double x = (a + b) * 2;
    double y = (a + b) * 3;
    double z = (a + b) * 4;
    return x + y + z;
}

❌ 没有优化的情况:

  • 计算 a + b 三次

✅ 经过 CSE 优化后:

java 复制代码
double temp = a + b;   // 只算一次!
double x = temp * 2;
double y = temp * 3;
double z = temp * 4;

2.4. 常量折叠

在编译期完成所有能提前计算的运算,避免运行时浪费 CPU。

代码如下所示:

java 复制代码
public int calculate() {
    return 3 + 5 * 2;        // 编译时就能算出是 13
}

JIT 优化过程:

  • 发现 352 都是常量
  • 表达式无副作用
  • → 直接替换成 return 13;

🎯 效果:省去运行时的乘法和加法指令。

常量折叠是"把能提前算的都算好",公共子表达式消除是"相同的活只干一次"。


3、验证 JIT

方法 1:加 JVM 参数打印编译日志

java 复制代码
java -XX:+PrintCompilation \
     -XX:+UnlockDiagnosticVMOptions \
     -XX:+PrintInlining \
     -XX:+PrintOptoAssembly \
     -XX:CompileCommand=print,*YourClass.yourMethod \
     YourApp

输出示例:

java 复制代码
      100   1       java.lang.String::hashCode (65 bytes)
      120   2       JITDemo::sum (17 bytes)
              ↳ 表示 sum 方法已被 JIT 编译

方法 2:性能对比实验

java 复制代码
// 关闭 JIT:纯解释执行(非常慢)
java -Xint JITDemo

// 开启 JIT:默认模式(越跑越快)
java JITDemo

// 强制只用 JIT(不解释)
java -Xcomp JITDemo

你会发现 -Xint 模式下程序明显变慢!


总结

优化技术 目的 原理简述 是否改变语义 典型场景
方法内联 减少方法调用开销 把小方法的代码"复制"到调用处 getter/setter、工具方法频繁调用
循环展开 减少循环控制开销 将多次迭代合并为一条语句执行 高频循环遍历数组或集合
常量折叠 提前计算常量表达式 在编译期算出 3 + 5 这类结果 数学公式、配置计算
公共子表达式消除 避免重复计算 相同表达式只算一次,结果复用 (a + b) 多次出现

参考文章:

1:https://blog.csdn.net/weixin_44183847/article/details/134803645?ops_request_misc=&request_id=&biz_id=102&utm_term=jvm%E7%9A%84%E5%8D%B3%E6%97%B6%E7%BC%96%E8%AF%91&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-134803645.142^v102^control&spm=1018.2226.3001.4187https://blog.csdn.net/weixin_44183847/article/details/134803645?ops_request_misc=&request_id=&biz_id=102&utm_term=jvm%E7%9A%84%E5%8D%B3%E6%97%B6%E7%BC%96%E8%AF%91&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-134803645.142^v102^control&spm=1018.2226.3001.4187

相关推荐
学Linux的语莫3 小时前
机器学习数据处理
java·算法·机器学习
西瓜er3 小时前
JAVA:Spring Boot 集成 FFmpeg 实现多媒体处理
java·spring boot·ffmpeg
你总是一副不开心的样子(´ . .̫ .3 小时前
一、十天速通Java面试(第三天)
java·面试·职场和发展·java面试
迎風吹頭髮3 小时前
UNIX下C语言编程与实践63-UNIX 并发 Socket 编程:非阻塞套接字与轮询模型
java·c语言·unix
我是华为OD~HR~栗栗呀4 小时前
23届考研-Java面经(华为OD)
java·c++·python·华为od·华为·面试
Javatutouhouduan4 小时前
Java程序员如何深入学习JVM底层原理?
java·jvm·java面试·后端开发·java架构师·java程序员·互联网大厂
m0_475064504 小时前
jvm中程序计数器
jvm
王嘉俊9254 小时前
设计模式--享元模式:优化内存使用的轻量级设计
java·设计模式·享元模式
ANYOLY4 小时前
JVM 线上调优与排查指南
jvm·测试工具