第一次学 Java 就被一句话洗脑:"一次编译,到处运行"。当时没多想,直到有一次在 Windows 上打完包,丢到 Linux 服务器上直接跑起来——才意识到这背后站着整整一个 JVM。这篇把 JVM 是什么、为什么能跨平台、一个 Java 程序从源码到跑起来经历了什么讲清楚,也作为 JVM 系列的总入口。
一、解决什么问题:Java 凭什么跨平台
先看 C 和 Java 的差别(机器码和字节码的完整对比见《机器码 vs 字节码:程序到底怎么被执行》):
| C/C++ | Java | |
|---|---|---|
| 编译产物 | 机器码(Windows 的 exe / Linux 的 ELF) | 字节码(.class,平台无关) |
| 能不能换平台跑 | ❌ 绑死编译时的平台 | ✅ 任何装了 JVM 的平台都能跑 |
| 谁执行 | 操作系统直接执行 | JVM 解释/编译后执行 |
C 的编译器把源码直接翻译成当前平台的机器码——Windows 上编译的 exe 到 Linux 上就是一堆乱码;Java 的 javac 只把源码翻译成字节码(.class),字节码不绑任何平台,执行字节码的是 JVM——而 JVM 每个平台都有一份。换平台不用重新编译,换个 JVM 就行。
二、JVM 是什么:一台"软件计算机"
JVM(Java Virtual Machine)是用软件模拟出来的一台计算机——它有自己的"指令集"(字节码指令)、自己的"内存"(运行时数据区)、自己的"CPU"(执行引擎)。你的 Java 代码其实不是跑在操作系统上,而是跑在 JVM 这台虚拟机器上。
两个概念要分开:
- JVM 规范:规定 JVM 应该长什么样(内存怎么分、字节码指令有哪些)——只是文档
- JVM 实现:真正跑起来的那个软件。主流是 HotSpot(Oracle/OpenJDK 默认),还有 OpenJ9(IBM)、GraalVM(高性能)等
三、一次 Java 程序的旅程(整体架构)
一个 Hello.java 从源码到跑起来,经历四大步:
① 编译:源码 → 字节码
javac Hello.java # 生成 Hello.class(字节码,平台无关)实测(javap -c 反编译看字节码长什么样):
public static void main(java.lang.String[]);
Code:
0: iconst_1 ← 把常量 1 压栈
1: istore_1 ← 存到局部变量 1(就是 int x = 1)
2: iconst_2
3: istore_2 ← int y = 2
4: getstatic ← 取 System.out
7: iload_1
8: iload_2
9: iadd ← x + y(整数相加指令)iconst/istore/iadd 这些就是字节码指令——不是机器码,是 JVM 的"汇编语言"。
② 类加载:字节码 → JVM 内存
JVM 启动后,类加载子系统把 .class 读进来:加载 → 链接(验证/准备/解析)→ 初始化。核心机制双亲委派:先让父加载器加载,父加载不了才自己加载——保证核心类(如 java.lang.String)永远由最顶层加载器加载,不会被篡改。
实测(打印类加载器层级):
String 类加载器:null ← null 就是 Bootstrap 加载器(最顶层)
自定义类加载器:ClassLoaders$AppClassLoader ← 应用类由 AppClassLoader 加载③ 运行时数据区:类进来后住哪
类加载完,对象、方法调用、类信息都放进运行时数据区——堆、栈、方法区、PC、本地方法栈。这块怎么分、怎么回收(GC),是 JVM 最核心的部分,单独成篇:《JVM 内存模型与垃圾回收》。
④ 执行引擎:跑起来
运行时数据区是"内存",执行引擎是"CPU":
- 解释器:把字节码逐条翻译成机器码执行(启动快,但每条都翻译)
- JIT 编译器:发现热点代码(循环、高频方法),整体编译成机器码缓存起来,下次直接跑机器码(越跑越快)
- GC:自动回收没用的对象(内存那篇讲)
实测 JIT 的力量(同一个 1 亿次循环,开不开 JIT):
默认(JIT 生效):25 ms
-Xint(纯解释器):426 ms ← 关了 JIT 慢 17 倍Java"慢"是几十年前的印象——HotSpot 的 JIT 把热点代码编译成本地机器码后,性能接近 C。这也是为什么线上应用要预热(压测打热再上线):刚开始解释执行慢,热点被 JIT 编译后速度才上来。
四、JVM 实现与版本
| JVM 实现 | 谁家的 | 特点 |
|---|---|---|
| HotSpot | Oracle / OpenJDK 默认 | 最主流,JDK 8~21 默认 |
| OpenJ9 | IBM | 启动快、内存省(云环境友好) |
| GraalVM | Oracle | 高性能 + 支持 AOT 编译成原生镜像 |
版本怎么选:认准 LTS(长期支持)版本——8、11、17、21。JDK 8 到 21 的最大变化之一是 JDK 9 起默认垃圾收集器从 Parallel 换成了 G1,很多网上旧文章还在按 JDK 8 讲,看资料时先确认版本。
五、JVM 知识地图
JVM 这篇是总入口,垂直话题都单独成篇:
JVM 是什么(本篇)——整体架构、跨平台、类加载、JIT
├── 内存与 GC: 《JVM 内存模型与垃圾回收》
├── 排查实战: 《内存溢出(OOM):类型、定位与实战排查》
├── 并发: 《Java 并发编程》
└── 高并发 IO: 《Java NIO 详解》小结
- JVM = 软件计算机:字节码是指令集,运行时数据区是内存,执行引擎是 CPU
- 跨平台:C 编译出机器码(绑平台),Java 编译出字节码(JVM 执行,处处可跑)
- 四大步:编译(javac→.class)→ 类加载(双亲委派)→ 运行时数据区 → 执行引擎(解释器 + JIT + GC)
- JIT:热点代码编译成机器码,实测快 17 倍——Java 不慢,记得预热
- 版本:HotSpot 默认,认准 LTS(8/11/17/21),JDK 9+ 默认 G1
内存分区、GC 回收原理见《JVM 内存模型与垃圾回收》;内存撑不住怎么排查见《内存溢出(OOM)》;JVM 之上的线程安全见《Java 并发编程》。
验证说明:
javap -c反编译字节码(iconst/istore/iadd 指令)、类加载器层级(String=Bootstrap 返回 null、自定义类=AppClassLoader)、JIT 对比(默认 25ms vs-Xint426ms,快 17 倍)全部 javac/java 本地实测。
