# 你的第一段 Java 代码，是怎么跑起来的

从源码到字节码再到 CPU 指令的两次翻译，以及 JDK/JRE/JVM 的真正关系

> JVM · 编译与字节码 · 跨平台原理 · 约 7 分钟 · 09 月 07 日

## 本篇要点

1. 一段 Java 程序要经过两次翻译才能运行：javac 编译器把 .java 源码编译成 .class 文件里的字节码，JVM 再把字节码翻译成当前 CPU 真正执行的机器指令。
2. 字节码是给“想象中的计算机”（JVM）看的指令，与操作系统和芯片型号无关，这也是它能在任何装了 JVM 的平台上运行的原因。
3. “一次编译，处处运行”的本质是把最后一次翻译推迟到运行时：编译到字节码只做一次，每个平台各自实现一个 JVM，由它负责把同一份字节码翻译成本地机器码。
4. JVM 执行字节码并非纯解释：HotSpot 会统计被反复调用的“热点”方法，用 JIT 编译器把它们编译成原生机器码缓存起来，所以长时间运行的 Java 程序往往越跑越快。
5. JDK、JRE、JVM 是层层包含的关系：JVM 只是执行引擎，JRE 加上核心类库才能运行程序，JDK 再加上 javac 等开发工具才能写程序；学习时直接安装 JDK 即可。

---

假设这是你接触 Java 的第一天。你照着教程敲下这样一段代码：

```java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("你好，Java！");
    }
}
```

把它存成 `HelloWorld.java`，然后在命令行里敲两个命令：

```
javac HelloWorld.java
java HelloWorld
```

屏幕就打印出“你好，Java！”。大多数初学者在这里就满足了，但这一瞬间其实发生了很多事：你的代码先是变成了一种你看不见的中间形态，再被一台“软件造出来的计算机”逐条执行，最终才变成 CPU 上的指令。本篇就把这趟旅程完整走一遍。至于 `public`、`static`、`main` 这些词为什么必须这么写，是下一篇的事，今天只回答一个问题：程序是怎么跑起来的。

## 一趟需要“两次翻译”的旅程

先说一个绕不开的事实：你的 CPU 听不懂 Java。CPU 只认识机器指令——由 0 和 1 组成的、针对特定芯片型号（如 Intel、ARM）的指令。不同平台的指令体系还互不相同。

所以任何高级语言写出的程序，想被 CPU 执行，都得经过“翻译”。C 语言的做法是一次翻译到底：编译器直接产出针对某个平台的机器码，换一个平台就得重新编译一次。Java 选择了另一条路：**翻译两次**。

```mermaid
flowchart LR
    A["HelloWorld.java<br/>源码：给人读"] -->|"javac 编译"| B["HelloWorld.class<br/>字节码：给 JVM 读"]
    B -->|"java 启动 JVM"| C["JVM 加载并执行字节码"]
    C -->|"解释执行 + JIT 优化"| D["CPU 执行机器指令"]
    D --> E["屏幕打印：你好，Java！"]
```

第一次翻译由 `javac` 完成，把源码变成字节码；第二次翻译发生在你运行程序的那一刻，由 JVM 完成，把字节码变成当前这台机器、这块 CPU 真正执行的指令。整个 Java 技术体系，就是围绕“中间那层字节码”设计的。下面一站一站看。

## 第一站：javac 把源码变成字节码

你敲下的第一个命令 `javac HelloWorld.java`，调用的是 JDK 里自带的编译器 `javac`。它逐行检查你的源码，如果没有语法错误，就生成一个新的文件 `HelloWorld.class`。

这个 `.class` 文件里装的不是机器码，而是**字节码（bytecode）**：一套给 JVM 看的指令。JVM 是 Java 设计者定义的一台“想象中的计算机”，它有自己的指令集——就像真实的 CPU 有它的指令集一样，只是这套指令不属于任何一家芯片厂商，也不依赖任何操作系统。指令之所以叫“字节码”，是因为每条指令的操作码只占一个字节 [4]。

你可以用 JDK 自带的 `javap` 工具偷看一眼 `.class` 内部长什么样：

```
javap -c HelloWorld
```

输出大致是：

```
public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7  // Field System.out
       3: ldc           #13 // String 你好，Java！
       5: invokevirtual #14 // Method PrintStream.println
```

`getstatic`（取一个静态字段）、`ldc`（把一个常量装入栈）、`invokevirtual`（调用一个方法）——这就是 JVM 的“语言”。你完全不需要读懂它，只要记住一件事：**`.class` 文件不是给 CPU 的，是给 JVM 的**。

## 第二站：JVM，一台软件造出来的计算机

第二个命令 `java HelloWorld` 启动了一个叫 JVM 的程序。JVM 本身是运行在你操作系统上的一个普通程序，但它模拟了一台完整的计算机：有自己的指令集，有自己的内存管理方式。Oracle 官方的定义是：JVM 是一台“抽象的计算机”，有指令集，并在运行时操作内存 [2]。

JVM 拿到 `HelloWorld.class` 后做三件事。首先是**类加载**：JVM 里的类加载器把 `.class` 文件读进内存，并检查字节码是否合法、安全——乱写的字节码会被当场拒绝，这是 Java 安全模型的一道闸门 [4]。

然后是**执行**。执行引擎读一条字节码指令、执行一条。但这里有个精妙之处：JVM 并不是从头到尾傻乎乎地逐条翻译。Java 的官方实现叫 HotSpot，意思是它会一边运行一边统计哪些方法被反复调用——这些是“热点”。刚开始，它用解释器把字节码一条条翻译成机器码执行，好处是启动快；一旦某个方法被调用足够多次，JIT（Just-In-Time，即时编译）编译器就把这段字节码整个编译成针对当前 CPU 的机器码缓存起来，下次直接运行优化过的机器码 [3]。

这就是为什么你常听到“Java 程序越跑越快”：一个长期运行的服务器程序，启动后先“解释”一阵子，随后热点代码被逐段编译成原生指令，性能接近甚至超过直接用 C 写的程序。

## “一次编译，处处运行”是怎么做到的

现在可以回答那个著名的口号了。Java 的跨平台，靠的是把“最后一次翻译”推迟到了运行时：

- **编译只做一次**：你在 Windows 上用 `javac` 编译出的 `HelloWorld.class`，拷贝到 Mac 或 Linux 上，不用重新编译，字节码是一模一样的。
- **JVM 每个平台各有一个**：Windows 上的 JVM、Mac 上的 JVM、Linux 上的 JVM，是不同的程序，由不同的人（或公司）针对各自平台开发。它们读的是同一份字节码，但翻译出来的机器码各不相同——那是各自 CPU 的事 [1][2]。

打个比方：字节码是一份用“世界语”写成的剧本，原样存档，谁也不用改。每个城市都配了一位口译员（JVM），开演时他把世界语剧本现场翻成当地方言（机器码），本地观众（CPU）这才听得懂。换一座城市，剧本不用重写，换一位口译员即可。

代价也要说清楚：多了一层翻译，程序启动时会比“一次编译成机器码”的语言稍慢；而且没有 JVM 的机器上跑不了 Java 程序。Java 的取舍是——用一点运行时开销，换来“一份代码处处能跑”。

## JDK、JRE、JVM：是三个东西还是一件事？

初学 Java 的第一道坎，是安装时撞见三个缩写。它们其实是层层包含的三个“同心圆”，从里到外是：

- **JVM**：最小的一层，真正执行字节码的那台“虚拟机”。只装它还不够，因为你的程序要用现成的类库（比如 `System.out.println` 里 `System` 这个现成类），这些代码不在 JVM 里。
- **JRE（Java Runtime Environment，Java 运行环境）** = JVM + 核心类库。普通用户要“运行”别人的 `.class` 程序，装 JRE 就够了。
- **JDK（Java Development Kit，Java 开发工具包）** = JRE + 开发工具，最重要的就是编译器 `javac`，还有 `javap`、调试器等。你要“开发”程序，必须装 JDK [1]。

![JDK 与 JRE 的关系示意（图片来自 Oracle 官方 Java 平台介绍页）](https://www.oracle.com/img/tech/java8-conceptual-design.jpg)

用厨房来记：JVM 是灶台，JRE 是灶台加齐备的锅碗瓢盆（能做菜了），JDK 还多一套菜刀和菜谱工具（能自己发明新菜了）。普通食客只要吃饭（运行程序），JRE 足够；你是来学做菜的，得装 JDK。如今的发行版也早已把两者打包：直接安装一个 JDK，运行所需的一切就都齐了，不用再单独找 JRE。

## 走到这里，你已经看到了全貌

把整条链再收一遍：你写的是给人读的 `.java` 源码；`javac` 把它编译成给 JVM 读的 `.class` 字节码；运行 `java` 命令时，本机安装的那个 JVM 把字节码加载进来，先解释执行、再把热点编译成机器码，最终你的 CPU 执行这些指令，屏幕上才出现那行字。

往后你学的每一个语法细节，都能在这张地图上找到位置：你写方法，是在给 JVM 准备可调用的指令序列；你用对象，是在指挥 JVM 管理内存。而下一篇文章，我们会回到开头那段 `HelloWorld`，把那句看起来像咒语一样的 `public static void main(String[] args)` 逐词拆开——你会看到，正是它规定了 JVM 该从哪里开始执行你的程序。

## 来源

1. [Oracle 官方 Java SE 平台介绍：JDK 是 JRE 的超集，JRE 提供 JVM 与类库，JVM 负责平台无关性](https://www.oracle.com/java/technologies/platform-glance.html)
2. [Oracle 官方文档：JRE 与 JDK 的定义、JVM 是带指令集的抽象计算机](https://docs.oracle.com/javase/8/docs/technotes/guides/index.html)
3. [Oracle 官方 Java VM 指南：HotSpot 自适应编译器先解释执行、再编译热点代码](https://docs.oracle.com/en/java/javase/25/vm/java-virtual-machine-guide.pdf)
4. [Azul 博客：javac 产出 .class 字节码、javap 反汇编示例与字节码指令集细节](https://www.azul.com/blog/understanding-java-compilation-from-bytecodes-to-machine-code/)

---

原文：https://pangzhengboyin.com/articles/how-java-programs-compile-and-run-696a0cd8

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
