JVMNotes

第 01 章:认识 JVM

zjc 于 2026-01-01 发布

这是《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 的核心职责:

  1. 加载 class 文件;
  2. 校验字节码;
  3. 分配对象内存;
  4. 执行字节码;
  5. JIT 优化热点代码;
  6. 回收不再使用的对象;
  7. 管理线程和锁;
  8. 提供诊断工具接口。

1.2 HotSpot JVM

Java 虚拟机规范定义了行为,不同厂商有不同实现。

常见 JVM:

JVM 特点
HotSpot OpenJDK / Oracle JDK 主流实现
OpenJ9 IBM 贡献,注重启动和内存
GraalVM 支持多语言和 AOT
Zulu / Corretto / Temurin OpenJDK 发行版

本书以 HotSpot 为主线。

OpenJDK 发行版选择建议:

  1. 优先选择当前团队已验证的 LTS;
  2. Java 17 / 21 是现代微服务常见选择;
  3. 新版本 GC、容器支持和语言特性更有优势;
  4. 升级前必须压测;
  5. 统一 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. 先委托父加载器;
  2. 父加载器无法加载再自己加载;
  3. 保证核心类库一致和安全;
  4. 避免重复加载;
  5. 某些场景需要打破,例如热部署和隔离。

1.5 对象创建

普通对象创建大致流程:

类加载检查
  -> 分配内存
  -> 初始化零值
  -> 设置对象头
  -> 执行 <init>

对象内存布局:

区域 内容
Object Header Mark Word、类型指针、数组长度
Instance Data 字段值
Padding 对齐填充

分配方式:

  1. 栈上分配,逃逸分析通过时可能优化;
  2. TLAB 分配,线程本地分配缓冲;
  3. Eden 分配;
  4. 大对象直接进入老年代或特殊区域。

1.6 执行引擎

JVM 有解释器和 JIT 编译器。

方式 特点
解释执行 启动快,单次执行慢
C1 Client 编译 编译快,优化中等
C2 Server 编译 编译慢,优化强
分层编译 常见默认,结合两者优点

JIT 优化包括:

  1. 方法内联;
  2. 逃逸分析;
  3. 锁消除;
  4. 循环优化;
  5. 去虚拟化;
  6. 公共子表达式消除。

这解释了为什么 Java 服务需要预热、为什么压测要从低流量逐步加压、为什么微小 benchmark 容易失真。

1.7 GC 初步

垃圾回收的核心问题:

  1. 哪些对象可以回收;
  2. 什么时候回收;
  3. 如何回收;
  4. 回收时应用是否停止;
  5. 停顿多长;
  6. 吞吐和延迟如何权衡。

常见收集器:

收集器 特点
Serial 单线程,适合小内存
Parallel 吞吐优先
CMS 老年代并发,Java 14 移除
G1 区域化、可预测停顿,常见默认
ZGC 低延迟大堆
Shenandoah 低延迟并发收集

选择建议:

  1. 默认 G1 是稳妥起点;
  2. 大堆低延迟可评估 ZGC;
  3. 吞吐型批处理可用 Parallel;
  4. 新版本能力要结合实际 JDK 支持;
  5. 不要凭感觉频繁切换 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 也可能来自堆外内存或进程限制。

思考题

  1. Java 源码到执行需要经历哪些阶段?
  2. JVM 进程内存为什么大于 -Xmx
  3. 分层编译为什么能兼顾启动性能和峰值性能?
  4. G1、ZGC、Parallel 各适合什么场景?
  5. 如果线上服务 CPU 100%,你会按什么顺序排查?