这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 JVM(Java Virtual Machine)是 Java 程序运行的虚拟机。它屏蔽底层操作系统差异,负责加载字节码、管理内存、执行指令、优化热点代码、调度线程和执行垃圾回收。
对生产系统来说,JVM 不是黑盒。理解 JVM 后,你才能回答:内存去了哪里、GC 为什么停顿、CPU 为什么高、线程为什么阻塞、容器为什么 OOM、应用为什么启动慢。
1.1 Java 程序如何运行
Hello.java
-> javac
-> Hello.class
-> ClassLoader
-> Runtime Data Areas
-> Interpreter / JIT
-> Native Execution
示例:
public class Hello {
public static void main(String[] args) {
int total = 0;
for (int i = 0; i < 100; i++) {
total += i;
}
System.out.println(total);
}
}
编译和运行:
javac Hello.java
java Hello
查看字节码:
javap -c Hello
JVM 的核心职责:
- 加载 class 文件;
- 校验字节码;
- 分配对象内存;
- 执行字节码;
- JIT 优化热点代码;
- 回收不再使用的对象;
- 管理线程和锁;
- 提供诊断工具接口。
1.2 HotSpot JVM
Java 虚拟机规范定义了行为,不同厂商有不同实现。
常见 JVM:
| JVM | 特点 |
|---|---|
| HotSpot | OpenJDK / Oracle JDK 主流实现 |
| OpenJ9 | IBM 贡献,注重启动和内存 |
| GraalVM | 支持多语言和 AOT |
| Zulu / Corretto / Temurin | OpenJDK 发行版 |
本书以 HotSpot 为主线。
OpenJDK 发行版选择建议:
- 优先选择当前团队已验证的 LTS;
- Java 17 / 21 是现代微服务常见选择;
- 新版本 GC、容器支持和语言特性更有优势;
- 升级前必须压测;
- 统一 JDK 供应商和版本。
1.3 运行时数据区
JVM 内存可以粗略分为线程共享区和线程私有区。
JVM Runtime
|-- Heap 线程共享
| |-- Young Generation
| | |-- Eden
| | |-- Survivor 0
| | +-- Survivor 1
| +-- Old Generation
|-- Method Area / Metaspace 线程共享
|-- Thread Stacks 线程私有
|-- PC Registers 线程私有
+-- Native Method Stacks 线程私有
| 区域 | 作用 | 常见错误 |
|---|---|---|
| Heap | 对象实例 | OutOfMemoryError: Java heap space |
| Metaspace | 类元数据 | Metaspace OOM |
| Stack | 栈帧、局部变量 | StackOverflowError |
| Direct Memory | 堆外内存 | Direct buffer memory OOM |
| Code Cache | JIT 编译代码 | CodeCache is full |
注意:堆不是 JVM 进程的全部内存。进程 RSS 还包括 Metaspace、线程栈、Code Cache、GC 结构、Direct Memory、Native 库和操作系统开销。
1.4 类加载初步
Java 类加载过程:
Loading
-> Linking
|-- Verification
|-- Preparation
+-- Resolution
-> Initialization
类加载器层次:
Bootstrap ClassLoader
-> Platform ClassLoader
-> Application ClassLoader
-> Custom ClassLoader
双亲委派的核心思想:
- 先委托父加载器;
- 父加载器无法加载再自己加载;
- 保证核心类库一致和安全;
- 避免重复加载;
- 某些场景需要打破,例如热部署和隔离。
1.5 对象创建
普通对象创建大致流程:
类加载检查
-> 分配内存
-> 初始化零值
-> 设置对象头
-> 执行 <init>
对象内存布局:
| 区域 | 内容 |
|---|---|
| Object Header | Mark Word、类型指针、数组长度 |
| Instance Data | 字段值 |
| Padding | 对齐填充 |
分配方式:
- 栈上分配,逃逸分析通过时可能优化;
- TLAB 分配,线程本地分配缓冲;
- Eden 分配;
- 大对象直接进入老年代或特殊区域。
1.6 执行引擎
JVM 有解释器和 JIT 编译器。
| 方式 | 特点 |
|---|---|
| 解释执行 | 启动快,单次执行慢 |
| C1 Client 编译 | 编译快,优化中等 |
| C2 Server 编译 | 编译慢,优化强 |
| 分层编译 | 常见默认,结合两者优点 |
JIT 优化包括:
- 方法内联;
- 逃逸分析;
- 锁消除;
- 循环优化;
- 去虚拟化;
- 公共子表达式消除。
这解释了为什么 Java 服务需要预热、为什么压测要从低流量逐步加压、为什么微小 benchmark 容易失真。
1.7 GC 初步
垃圾回收的核心问题:
- 哪些对象可以回收;
- 什么时候回收;
- 如何回收;
- 回收时应用是否停止;
- 停顿多长;
- 吞吐和延迟如何权衡。
常见收集器:
| 收集器 | 特点 |
|---|---|
| Serial | 单线程,适合小内存 |
| Parallel | 吞吐优先 |
| CMS | 老年代并发,Java 14 移除 |
| G1 | 区域化、可预测停顿,常见默认 |
| ZGC | 低延迟大堆 |
| Shenandoah | 低延迟并发收集 |
选择建议:
- 默认 G1 是稳妥起点;
- 大堆低延迟可评估 ZGC;
- 吞吐型批处理可用 Parallel;
- 新版本能力要结合实际 JDK 支持;
- 不要凭感觉频繁切换 GC。
1.8 线程与 Java 内存模型
JVM 线程映射到操作系统线程。
查看 Java 线程:
jstack PID
jcmd PID Thread.print
Java 内存模型(JMM)定义共享变量的可见性、有序性和原子性规则。
核心概念:
| 概念 | 说明 |
|---|---|
| happens-before | 操作可见性规则 |
| volatile | 可见性和禁止重排 |
| synchronized | 互斥和内存语义 |
| CAS | 原子比较交换 |
| final | 安全发布语义 |
并发 bug 往往不是必现,而是依赖时序、缓存和编译器优化。
1.9 生产 JVM 诊断工具
常用命令:
java -version
jps -l
jstat -gcutil PID 1000
jmap -histo PID
jcmd PID VM.flags
jcmd PID GC.class_histogram
jstack PID
jcmd PID JFR.start duration=120s filename=app.jfr
外部工具:
| 工具 | 用途 |
|---|---|
| Arthas | 在线诊断、watch、trace、火焰图 |
| JFR + JMC | 低开销飞行记录 |
| MAT | 堆 dump 分析 |
| VisualVM | 图形化观察 |
| async-profiler | CPU / alloc 火焰图 |
本章小结
JVM 负责加载字节码、管理内存、执行和优化代码、调度线程并回收对象。理解运行时数据区、类加载、执行引擎、GC 和线程模型,是诊断生产问题的基础。JVM 内存不只等于堆,容器 OOM 也可能来自堆外内存或进程限制。
思考题
- Java 源码到执行需要经历哪些阶段?
- JVM 进程内存为什么大于
-Xmx? - 分层编译为什么能兼顾启动性能和峰值性能?
- G1、ZGC、Parallel 各适合什么场景?
- 如果线上服务 CPU 100%,你会按什么顺序排查?