JVM 是 Java 跨平台能力的根基:源码先编译成平台无关的字节码,再由各平台上的虚拟机实现负责执行。这篇文章讲清三件事——JDK、JRE、JVM 的关系,HotSpot 的即时编译机制,以及「一次编译,到处运行」的真实含义。

JDK、JRE、JVM 是什么关系

三者是逐层包含的关系:

  • JVM(Java Virtual Machine):运行字节码的虚拟机,负责类加载、内存管理、垃圾回收和指令执行。它本质上是一套规范,HotSpot 是应用最广的实现,此外还有 OpenJ9、GraalVM 等。
  • JRE(Java Runtime Environment):JVM 加上运行所需的 Java 核心类库(如 java.lang、java.util),只够把程序”跑起来”。
  • JDK(Java Development Kit):JRE 再加上开发工具链——编译器 javac、诊断工具 jstack、jmap 等,面向开发者。

日常说”装 Java”,装的都是 JDK。JDK 9 引入模块化之后,还可以用 jlink 裁剪出只包含所需模块的精简运行时,传统意义上独立分发的 JRE 已经逐步淡出。

「一次编译,到处运行」的真实含义

这句话更准确的说法是:一次编译成字节码,字节码可以交给任何平台上的 JVM 执行。真正跨平台的不是 Java 程序本身,而是字节码这个中间层——每个操作系统都有各自的 JVM 实现,由它把统一格式的字节码翻译成当前平台的机器指令,平台差异被 JVM 这一层吸收掉了。

代价体现在三个方面:

  1. 性能上的间接成本:字节码不是机器码,不能直接交给 CPU,要么逐条解释执行,要么在运行时动态编译成本地代码。这带来启动慢、需要预热才能到达峰值性能的问题,也是 JIT 存在的根本原因。
  2. 行为差异并未消失:不同平台、不同 JVM 实现、甚至同一 JVM 的不同版本,在默认参数、线程调度、文件编码等细节上并不完全一致,跨平台部署仍然需要在目标环境实测。
  3. 跨平台不是无条件的:一旦通过 JNI 调用本地库,或依赖特定操作系统的特性,代码就被绑死在对应平台上。

更务实的理解是:JVM 抹平了大部分平台差异,但没有消除验证和适配的工作。

HotSpot 与即时编译

HotSpot 的名字来自它的核心思路:与其把所有代码都编译一遍,不如只把”热点”代码编译好。

解释执行与编译执行

程序启动时,字节码先由解释器逐条执行,优点是启动快。执行过程中,JVM 统计方法的调用次数和循环回边次数,超过阈值的”热点代码”被提交给即时编译器(JIT,Just-In-Time Compiler)编译成本地机器码并缓存,后续调用直接走本地代码。这种”边运行边编译”的模式,让 Java 程序往往越跑越快。

C1 与 C2 编译器

HotSpot 内置两个编译器,定位不同:

  • C1(客户端编译器):编译速度快、优化少,追求尽快用上本地代码。
  • C2(服务端编译器):编译耗时,但会做方法内联、逃逸分析、锁消除、循环展开等激进优化,产出高质量机器码,是峰值性能的来源。

分层编译

早期需要在 C1(-client)和 C2(-server)之间二选一。JDK 7 引入、JDK 8 起默认开启分层编译(Tiered Compilation),两者协作,代码按热度逐级升迁:

  1. 第 0 层:解释执行,收集 profiling 信息;
  2. 第 1~3 层:C1 编译,分别对应不带 profiling、带少量 profiling、带完整 profiling 三种程度;
  3. 第 4 层:C2 编译,利用前期收集的运行数据做激进优化。

值得注意的一点是:C2 的激进优化建立在运行时统计的假设上,假设一旦被推翻(比如内联的方法出现了新的子类实现),JVM 会执行”去优化”,退回解释执行重新收集数据。JIT 编译是动态的、可回退的,而不是一锤子买卖。

小结

  • JDK 包含 JRE,JRE 包含 JVM;JVM 是规范,HotSpot 是最常用的实现。
  • 跨平台的是字节码,代价是解释与运行时编译的间接成本,以及平台差异带来的验证成本。
  • HotSpot 用解释器保证启动速度,用 C1、C2 分层编译换取峰值性能,热点探测与去优化机制让它始终基于真实运行数据做决策。