引言

为什么要学习这个主题?

你是否曾经好奇:

  • 为什么Java程序能够"一次编写,到处运行"?
  • 一个简单的 System.out.println("Hello World") 背后发生了什么?
  • .java 文件和 .class 文件有什么区别?

作为JVM学习的第一课,理解Java程序的运行机制是后续所有知识的基础。就像学习汽车驾驶前需要了解汽车的基本构造一样,学习JVM调优前也需要先理解Java程序是如何运行的。

你将学到什么?

  • ✅ Java程序从源代码到执行的完整流程
  • ✅ JVM在Java运行机制中的核心作用
  • ✅ “一次编写,到处运行"的底层原理
  • ✅ 编译型语言 vs 解释型语言,Java是什么?

一、从HelloWorld开始

1.1 最简单的Java程序

// HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

执行这个程序需要两个步骤:

# 第一步:编译
javac HelloWorld.java

# 第二步:运行
java HelloWorld

输出

Hello, World!

看起来很简单,但这两行命令背后隐藏着Java运行机制的全部秘密。

1.2 编译后发生了什么?

执行 javac HelloWorld.java 后,目录中会生成一个新文件:

HelloWorld.java   # 源代码(Java语言)
HelloWorld.class  # 字节码(JVM语言)

关键问题:为什么不是直接生成机器码(如 .exe 文件)?

这就是Java与C/C++等传统编译型语言的核心区别。


二、Java程序的两阶段运行模型

2.1 传统编译型语言(如C语言)

源代码(.c) → [C编译器] → 机器码(.exe) → [CPU直接执行]

特点

  • 编译后生成特定平台的机器码
  • Windows编译的程序不能在Linux上运行
  • 执行速度快(直接被CPU执行)
  • 无法跨平台

2.2 Java的两阶段模型

源代码(.java) → [javac编译器] → 字节码(.class) → [JVM解释/编译] → 机器码 → [CPU执行]

特点

  • 第一阶段:编译成平台无关的字节码
  • 第二阶段:JVM将字节码转换为特定平台的机器码
  • 关键:字节码可以在任何安装了JVM的平台上运行

2.3 流程图

┌─────────────────┐
│  HelloWorld.java │  ← Java源代码(人类可读)
└────────┬────────┘
         │ javac编译器
         ↓
┌─────────────────┐
│ HelloWorld.class│  ← 字节码(JVM可读)
└────────┬────────┘
         │ 运行时(java命令)
         ↓
┌─────────────────┐
│      JVM        │  ← Java虚拟机(核心角色)
│  ┌───────────┐  │
│  │类加载器    │  │  ← 加载.class文件
│  ├───────────┤  │
│  │字节码校验  │  │  ← 安全检查
│  ├───────────┤  │
│  │解释器/JIT  │  │  ← 转换为机器码
│  └───────────┘  │
└────────┬────────┘
         │
         ↓
┌─────────────────┐
│   操作系统       │
│   CPU执行       │
└─────────────────┘

三、核心概念深度解析

3.1 什么是字节码?

字节码(Bytecode):一种中间表示形式,既不是源代码,也不是机器码。

查看字节码内容:

javap -c HelloWorld.class

输出示例

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

这些类似 getstaticldcinvokevirtual 的指令就是JVM指令,是JVM能够理解的"语言”。

为什么需要字节码?

  1. 平台无关性:同一份字节码可以在Windows、Linux、macOS上运行
  2. 安全性:JVM可以在加载时验证字节码的合法性
  3. 优化空间:JVM可以在运行时对热点代码进行优化(JIT编译)

3.2 JVM的核心作用

JVM就像是Java程序和操作系统之间的"翻译官":

Java程序:老板,我要在屏幕上打印"Hello World"
    ↓
JVM:收到!让我看看...
    → Windows系统:调用 kernel32.dll
    → Linux系统:调用 glibc
    → macOS系统:调用 CoreFoundation

核心职责

  1. 加载字节码:读取 .class 文件
  2. 校验字节码:确保代码安全(防止恶意代码)
  3. 执行字节码
    • 解释执行(逐行翻译成机器码)
    • JIT编译(将热点代码编译成机器码缓存)
  4. 内存管理:自动分配和回收内存(垃圾回收)

3.3 “一次编写,到处运行"的实现

       同一份字节码
            ↓
    ┌───────┴───────┐
    ↓               ↓
Windows JVM      Linux JVM
    ↓               ↓
Windows机器码    Linux机器码
    ↓               ↓
 Windows CPU     Linux CPU

关键点

  • 开发者只需编译一次(javac)
  • JVM负责适配不同平台
  • 开发者无需关心底层操作系统和CPU架构

四、Java是编译型还是解释型语言?

4.1 传统定义

  • 编译型语言:C、C++、Go(编译成机器码)
  • 解释型语言:Python、JavaScript(逐行解释执行)

4.2 Java的特殊性

Java是两者的结合

编译阶段(javac):Java源码 → 字节码(编译型)
运行阶段(JVM):  字节码 → 机器码(解释型 + JIT编译)

更准确的描述:Java是一种混合型语言

  • 第一阶段:编译成字节码(类似编译型)
  • 第二阶段
    • 初期:解释执行字节码(类似解释型)
    • 后期:JIT编译热点代码(变成编译型)

4.3 性能对比

纯解释执行 < Java(解释+JIT) < C/C++(纯编译)
   慢             中等              快

实际情况

  • 启动时:Java较慢(需要加载JVM和类)
  • 预热后:Java接近C++性能(JIT编译优化)

五、实战:观察Java程序的执行过程

5.1 查看编译产物

# 编译
javac HelloWorld.java

# 查看文件大小
ls -lh HelloWorld.*

输出

-rw-r--r--  1 user  staff   119B Nov 21 20:00 HelloWorld.java
-rw-r--r--  1 user  staff   426B Nov 21 20:01 HelloWorld.class

分析:字节码文件比源代码还大,因为包含了:

  • 常量池
  • 方法表
  • 字节码指令
  • 元数据信息

5.2 查看JVM进程

终端1:运行一个持续运行的程序

public class LoopTest {
    public static void main(String[] args) throws Exception {
        while (true) {
            System.out.println("Running...");
            Thread.sleep(1000);
        }
    }
}

终端2:查看JVM进程

jps -l

输出

12345 LoopTest
12346 sun.tools.jps.Jps

分析

  • 每个Java程序都运行在一个JVM进程中
  • jps 是JDK自带的工具(Java Process Status)

六、常见误区与疑问

❓ 误区1:Java程序是解释执行的,所以很慢

真相

  • 早期的JVM确实主要依赖解释执行,性能较差
  • 现代JVM(如HotSpot)拥有强大的JIT编译器
  • 热点代码会被编译成本地机器码,性能接近C++
  • 结论:现代Java的性能已经不是问题

❓ 误区2:JVM只能运行Java语言

真相

  • JVM运行的是字节码,而不是Java源代码
  • 任何能编译成字节码的语言都能在JVM上运行
  • JVM语言家族
    • Java(主流)
    • Kotlin(Android开发)
    • Scala(大数据)
    • Groovy(动态语言)
    • Clojure(函数式编程)

❓ 疑问3:为什么不直接编译成机器码?

原因

  1. 跨平台性:字节码在所有平台上相同
  2. 安全性:JVM可以在加载时验证代码
  3. 动态性:支持动态类加载、反射等特性
  4. 优化空间:JIT可以根据运行时情况优化

代价

  • 启动较慢(需要加载JVM)
  • 内存占用较高(JVM本身需要内存)

总结

核心要点回顾

  1. Java程序运行的两阶段模型

    • 编译阶段:javac.java 编译成 .class(字节码)
    • 运行阶段:java 命令启动JVM,JVM加载并执行字节码
  2. JVM的核心作用

    • 加载和校验字节码
    • 将字节码转换为机器码(解释执行 + JIT编译)
    • 提供内存管理和垃圾回收
  3. “一次编译,到处运行"的实现

    • 字节码是平台无关的中间表示
    • 不同平台的JVM负责将字节码转换为对应的机器码
  4. Java的语言特性

    • 既不是纯编译型,也不是纯解释型
    • 是编译型和解释型的混合体
    • 现代JVM的性能已接近传统编译型语言

思考题

  1. 如果删除 .java 源文件,只保留 .class 文件,程序还能运行吗?为什么?
  2. 一个32位系统上编译的 .class 文件能在64位系统上运行吗?
  3. 为什么有些Java程序启动很慢,但运行一段时间后变快了?

下一篇预告

下一篇文章《JVM到底是什么?虚拟机的本质》,我们将深入探讨:

  • 什么是虚拟机?
  • JVM规范 vs JVM实现
  • 主流JVM对比:HotSpot、OpenJ9、GraalVM
  • JVM的内存模型和组件架构

参考资料


欢迎在评论区分享你的学习心得或提出疑问!