Skip to content

Java 代码执行流程

从一行代码到屏幕输出

你有没有想过,当你在 IDE 里点击「运行」,到屏幕上出现 Hello World,JVM 到底经历了什么?

我们从一个最简单的例子开始:

java
public class HelloJVM {
    public static void main(String[] args) {
        System.out.println("Hello, JVM");
    }
}

这一行代码的背后,是一次完整的 JVM 旅程。

全流程概览

源代码
  (.java)

    │ javac 编译

字节码文件
  (.class)

    │ 类加载器

运行时数据区
  (加载到内存)

    │ 执行引擎

本地机器码
  (CPU 执行)


程序输出

看起来只有 5 步,但每一步背后都藏着大量细节。

第一步:编写源代码

java
public class HelloJVM {
    public static void main(String[] args) {
        System.out.println("Hello, JVM");
    }
}

.java 文件是人类用高级语言写的文本。机器不认识它,JVM 也不认识它。第一步要把人类语言翻译成机器能理解的中间语言——字节码。

第二步:编译成字节码

bash
javac HelloJVM.java

javac 是 Java 编译器。它的任务是把 Java 源代码翻译成 JVM 字节码.class 文件)。

字节码不是机器码,而是 JVM 指令的二进制表示。看看编译后生成了什么:

bash
xxd HelloJVM.class | head -20

输出大致如下(十六进制):

00000000: cafe babe 0000 0037 001a 0a00 0500 0f09  .......7........
00000010: 0003 000f 0800 100a 0011 0012 0700 1307  ................
00000020: 0014 0c00 0800 0907 0015 0c00 1600 1701  ................
00000030: 001c 6865 6c6c 6f20 776f 726c 6401 0006  ...hello world..

开头的 cafe babe 是 JVM 规定的魔数(magic number),所有 Class 文件都以这四个字节开头。JVM 通过这个标记判断文件是否合法。

小知识:据说 James Gosling 在选择这个魔数时说,「Java」这个名字和爪哇咖啡有关,所以选了一个咖啡馆会有的数字:CAFE BABE(咖啡馆里的靓仔)。这当然是个玩笑,但这个约定一直延续到今天。

字节码能用文本方式查看吗?

可以,用 javap -c 反汇编:

java
Compiled from "HelloJVM.java"
public class HelloJVM {
  public HelloJVM();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello, JVM
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

这就是 System.out.println("Hello, JVM") 的字节码表示。每条指令都对应一个 JVM 指令。

第三步:类加载

运行 java HelloJVM 时,JVM 启动,类加载器开始工作。

类加载的过程

  1. Loading(加载):找到 .class 文件,读取字节流,创建一个 Class 对象
  2. Linking(链接)
    • 验证:字节码格式是否合法
    • 准备:为类的静态变量分配内存并设置默认值
    • 解析:把符号引用替换为直接引用(如 #2 → 实际的 System.out 对象)
  3. Initialization(初始化):执行 <clinit> 方法,给静态变量赋予真正的初始值

类加载不是一次性完成的。JVM 遵循懒加载原则:只有当代码真正用到某个类时,这个类才会被加载。比如 System 类在一开始就被引用了,所以会先加载进来。

类加载器层级

引导类加载器(Bootstrap ClassLoader)
  └── 加载 JAVA_HOME/lib 下的核心类库


扩展类加载器(Extension ClassLoader)
  └── 加载 JAVA_HOME/lib/ext 下的扩展类


应用类加载器(Application ClassLoader)
  └── 加载 classpath 下的用户类


自定义类加载器(可选)

一个类加载请求,会从下往上先问问「父母」是否加载过,再从上往下逐层尝试加载。这就是双亲委派模型

第四步:执行引擎

字节码被加载到内存后,执行引擎开始工作。

解释执行 vs JIT 编译

解释执行:JVM 是一条一条解释字节码并执行的。就像同声传译,说一句翻译一句。

java
// JVM 内部大致是这样的循环
while (hasMoreInstructions()) {
    Instruction ins = fetchNextInstruction();
    execute(ins);  // 把字节码指令「翻译」成机器码执行
}

JIT 编译:HotSpot 会监控哪些代码被反复执行(热点代码),把热点代码编译成本地机器码缓存起来,以后直接执行编译后的代码。

java
// 热点探测:被调用 1000 次以上,或循环回边 10000 次以上
if (isHotSpot(method)) {
    compiledCode = JITCompiler.compile(method);
    cache(compiledCode);
    execute(compiledCode);
}

JIT 编译器又有两个:

  • C1 编译器(Client Compiler):编译速度快,优化较少,适合桌面应用
  • C2 编译器(Server Compiler):编译速度慢,优化激进,适合服务器长时间运行

HotSpot 默认使用分层编译(Tiered Compilation):先用 C1 快速编译让程序跑起来,再慢慢用 C2 深度优化。

第五步:本地机器码执行

JIT 编译后的本地机器码被交给 CPU 执行。CPU 执行机器指令,访问内存,最终输出结果。

这是一个完整的闭环:

源代码 (.java)
      ↓ javac
字节码 (.class)
      ↓ 类加载器
内存中的 Class 对象
      ↓ 执行引擎
本地机器码
      ↓ CPU
程序输出

运行时数据区的状态

在程序执行过程中,JVM 运行时数据区发生了什么?以 System.out.println("Hello, JVM") 为例:

PC 寄存器:指向当前字节码指令位置(第 3 条:ldc #3)

虚拟机栈:main 方法的栈帧
         ├── 局部变量表:[args = 引用类型数组]
         ├── 操作数栈:[栈顶: "Hello, JVM" 字符串引用]
         └── 动态链接:指向常量池中符号引用的解析结果

堆:字符串常量 "Hello, JVM"(字符串池中的对象)

方法区/元空间:System、PrintStream 等类的元信息

每个线程都有自己的 PC 寄存器和虚拟机栈,但堆和方法区是所有线程共享的。

方法调用与栈帧

main 方法调用 println 时,JVM 在 main 的栈帧之上,创建一个新的 println 栈帧:

┌─────────────────┐
│  println 栈帧    │  ← 当前执行位置
│  ├─ 操作数栈     │
│  ├─ 局部变量表   │
│  └─ 返回地址    │
├─────────────────┤
│  main 栈帧       │
│  ├─ 操作数栈     │
│  ├─ 局部变量表   │
│  └─ 返回地址    │
└─────────────────┘

println 执行完后,它的栈帧弹出,main 栈帧恢复执行。这个过程叫方法调用栈

本节小结

Java 代码的执行流程:

  1. 编写.java 源代码
  2. 编译javac 把源码编译成 .class 字节码文件
  3. 加载 → 类加载器把字节码加载进 JVM,创建 Class 对象
  4. 执行 → 执行引擎解释或 JIT 编译字节码
  5. 输出 → 本地机器码交给 CPU 执行,结果输出到屏幕

理解这个流程,有助于理解 JVM 各个模块是怎么协同工作的。后续章节会逐一深入每个环节。

下一节,我们来聊聊 指令集架构,看看为什么 JVM 选择基于栈的设计,以及栈式架构和寄存器架构各有何优劣。

基于 VitePress 构建