Skip to content

第一次学 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 从源码到跑起来,经历四大步:

JVM 整体架构:源码 → 字节码 → 类加载 → 内存 → 执行引擎

① 编译:源码 → 字节码

bash
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 实现谁家的特点
HotSpotOracle / OpenJDK 默认最主流,JDK 8~21 默认
OpenJ9IBM启动快、内存省(云环境友好)
GraalVMOracle高性能 + 支持 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 -Xint 426ms,快 17 倍)全部 javac/java 本地实测