Skip to content

编译 vs 解释运行(机器码/指令/汇编)

两条路:编译执行与解释执行

理解 JVM 的执行方式,需要先理解计算机执行代码的两条基本路径:编译执行解释执行

编译执行:提前把所有代码翻译好

编译型语言(如 C、C++、Go)在程序运行之前,先把源代码一次性全部翻译成本地机器码,生成可执行文件。运行时直接执行机器码。

源代码 (.c)  ──→  编译器  ──→  可执行文件 (.exe)


                            直接交给 CPU 执行
                            (不再需要编译器)

编译型语言的特点

特点说明
执行速度快(直接执行机器码)
启动速度快(不需要编译步骤)
跨平台差(需要针对每个平台分别编译)
内存占用小(没有运行时解释器)
典型代表C、C++、Go、Rust

解释执行:边跑边翻译

解释型语言(如 Python、Ruby、JavaScript)在程序运行过程中,由解释器逐条读取源代码或中间表示,逐条翻译成机器码并执行。

源代码 (.py)  ──→  解释器  ──→  读取一行
                                    │ 翻译一行
                                    ▼ 执行一行

                                    ← (循环)

解释型语言的特点

特点说明
执行速度慢(每次都需要翻译)
启动速度快(不需要预先编译)
跨平台好(只要有解释器)
内存占用大(需要解释器常驻)
典型代表Python、Ruby、JavaScript

Java 的特殊路径:中间字节码

Java 走了一条中间路线:

源代码 (.java)


前端编译器(javac)


字节码 (.class)

        ├──→ 解释执行(启动时)

        └──→ JIT 编译执行(热点代码)


            本地机器码


            CPU 执行

Java 既不是纯编译型,也不是纯解释型。它的字节码是「编译+解释」的产物:

  • javac:把 .java 编译成 .class(编译阶段)
  • JVM 解释器:把 .class 解释成机器码(解释执行)
  • JIT 编译器:把热点 .class 编译成本地机器码(编译执行)

机器码、字节码、汇编

这三个概念经常被混淆,一图说清楚:

┌──────────────────────────────────────────────────────────────┐
│                    编译执行 vs 解释执行                          │
│                                                               │
│  C 代码 ──→ 编译器 ──→ 机器码 ──→ CPU 直接执行                  │
│          (运行前)     (010101...)                             │
│                                                               │
│  Java 代码 ──→ javac ──→ 字节码 ──→ 解释器 ──→ 机器码 ──→ CPU   │
│             (运行前)     (class)  (运行时)   (010101...)        │
│                                                               │
│  字节码:JVM 的「机器码」,面向 JVM,不针对任何具体 CPU          │
│  机器码:操作系统的「原生码」,针对具体 CPU 架构(x86/ARM)     │
└──────────────────────────────────────────────────────────────┘
概念说明例子
机器码CPU 能直接执行的二进制指令,0101...0x55 0x48 0x8B
字节码JVM 的中间指令,面向 JVM,不针对具体硬件iconst_1iload_2
汇编机器码的文本表示,一一对应mov rax, rbx

字节码与机器码的区别

维度JVM 字节码本地机器码
目标平台JVM(软件抽象)x86/ARM/RISC-V(硬件)
存储格式.class 文件可执行文件
指令长度变长(1~9 字节)固定/变长(取决于架构)
架构统一(所有平台相同)不统一(x86 ≠ ARM)
执行方式解释或 JIT 编译直接执行
可移植性高(只依赖 JVM)低(依赖 CPU)

字节码的具体形态

javap -c 看看字节码长什么样:

java
public class ByteCodeDemo {
    public static int add(int a, int b) {
        return a + b;
    }
}

编译后:

java
public static int add(int, int);
    Code:
      0: iload_0        // 加载局部变量 slot 0(参数 a)
      1: iload_1        // 加载局部变量 slot 1(参数 b)
      2: iadd           // 相加
      3: ireturn        // 返回 int

对应的 x86 机器码可能是(objdump 反汇编):

asm
push   %rbp
mov    %rsp,%rbp
mov    %edi,-0x4(%rbp)   ; 存 a
mov    %esi,-0x8(%rbp)   ; 存 b
mov    -0x4(%rbp),%eax    ; 取 a
add    -0x8(%rbp),%eax    ; 加 b
pop    %rbp
ret

编译优化:JIT 的威力

JIT 编译器之所以比纯解释执行快,一个重要原因是编译时可以进行大量优化

java
public class JITOptimization {
    static int compute() {
        int sum = 0;
        for (int i = 0; i < 1000000; i++) {
            sum += i;
        }
        return sum;
    }
}

解释执行时,JVM 每次循环都要:取指令 → 解码 → 查表 → 执行。

JIT 编译后,这段代码可能变成直接执行编译后的机器指令,没有了解释的中间开销。而且 JIT 编译器还会做更多优化:

优化类型说明
内联把方法调用替换为方法体,减少调用开销
死代码消除删除永远不会执行的代码
常量折叠1 + 2 直接替换成 3
逃逸分析分析对象是否逃逸,决定栈上分配还是堆分配
向量化将多个标量操作合并成 SIMD 向量指令

预编译(AOT):编译的另一种形态

JDK 9 引入的 AOT(Ahead-Of-Time)编译,是第三条路:

源代码 (.java)


jaotc 编译器(JDK 9+)


本地机器码(.so / .dll)


JVM 加载时直接使用预编译的机器码
(跳过 JIT 编译阶段)
bash
# JDK 9+ 使用 jaotc 预编译
jaotc --output lib.so MyApp.class
java -XX:AOTLibrary=./lib.so MyApp

AOT 和 JIT 的对比:

维度JIT 编译AOT 编译
编译时机运行时(程序运行中)运行时之前(程序启动前)
编译优化基于运行时信息(profile-guided)无运行时信息
启动速度慢(需要预热)快(直接使用预编译代码)
峰值性能高(JIT 可根据运行时信息优化)较低(无运行时反馈)
适用场景服务器长期运行应用短期程序、容器镜像

本节小结

编译与解释的三种形态:

模式编译时机代表特点
纯编译运行前C、C++快,但不可移植
纯解释运行时Python、Ruby可移植,但慢
Java 混合运行时Java(解释+JIT)启动快 + 峰值快

Java 的字节码是「可移植的中间态」,JIT 编译器在此基础上实现「启动快 + 运行快」的双重目标。

下一节,我们来看 解释器与 JIT 编译器并存原因

基于 VitePress 构建