1. 项目背景
业务场景:某电商公司的订单系统在"双十一"大促期间频繁出现 Full GC 停顿,P99 延迟从日常的 50ms 飙升至 3000ms。运维团队紧急扩容 20 台机器才勉强扛住流量。事后复盘会上,开发说"堆内存不够",运维说"元空间炸了",测试说"应该是 Safepoint 卡住了"——同一个故障,三个角色三种说法,谁也说服不了谁。
痛点:团队缺乏统一的 JVM 术语体系和架构认知。具体表现为:
- 术语混乱:开发口中的"方法区",运维理解为"Metaspace",而测试在 JDK 7 的老文档里看到的是"永久代",三者对同一概念的理解不在一个时区。
- 架构盲区:当有人提到"OopMap 决定了 GC Root 的枚举"时,半数与会者表示没听过 Oop 这个缩写。
- 排障低效:每次线上故障,大家从不同知识起点出发辩论,30 分钟过去了还没达成共识该看哪份日志。
本章作为专栏的开篇,目标是在团队中建立一套统一的 JVM 术语"黑话词典"和对 OpenJDK 整体架构的直觉认知。学完本章后,开发、运维、测试至少能在故障发生时"用同一种语言交流"。
2. 项目设计
(会议室里,投影仪上显示着上周的 Full GC 监控曲线,三人围坐。)
小胖(咬了一口面包):大师,我就想不通——咱们写的 Java 代码不就是 .java 文件嘛,编译成 .class 扔给 JVM 跑就行了,为啥还要扯什么"运行时数据区""执行引擎""垃圾收集器"……这不就跟食堂打饭一样,你递个碗,阿姨给你盛饭,有啥复杂的?
大师(笑了笑):你这个比喻方向对了,但漏掉了最关键的两点。食堂打饭的时候,你要先刷卡验证身份对不对?阿姨要根据你的碗大小决定盛多少——万一碗太大,她还得换个大勺。JVM 做的事情比食堂阿姨多得多:它要先验证你的 .class 文件是否合法(类加载),再分配一块内存来放你的数据(堆),然后用解释器或者编译器把字节码变成机器指令(执行引擎),最后还得回收你用过的碗(GC)。
小白(推了推眼镜):等一下,每个环节我都听过一些术语,但总觉得它们是断裂的。比如"oop"到底是什么?我见过代码里写 oopDesc,它是"object"的缩写吗?为什么不用 Object 这个更常见的缩写?
大师:好问题。oop 全称是 Ordinary Object Pointer,是 HotSpot VM 中表示 Java 对象的内部数据结构。为什么叫"Ordinary"?因为 HotSpot 内部还有一种叫类对象的东西——Klass,它描述类的元信息。你可以这样理解:oop 是"每本书的内容",Klass 是"这本书的目录和封面信息"。一个 Java 对象在堆里是一个 oop,而它的类信息(比如方法表、字段布局)存在 Klass 实例里。
技术映射:oop ↔ Java 对象在 VM 层的表示;Klass ↔ Java 类的 VM 层元数据;两者合起来就是"实例 + 类型"的经典二元模型。
小胖(眼睛亮了):哦,那"堆"我懂,就是放对象的那个大池子。但是"Metaspace"、"永久代"、"方法区"这三个词,我经常搞混。
大师:这是历史遗留问题,很多人栽在这里。"方法区"(Method Area)是 JVM 规范里定义的逻辑概念——存放类信息、常量、静态变量、JIT 编译后的代码。而"永久代"(PermGen)是 HotSpot JDK 7 及以前对这个逻辑概念的物理实现——把方法区放在 JVM 管理的堆内存中。JDK 8 开始,HotSpot 用"Metaspace"取代了永久代,元数据被移到本地内存(Native Memory),不再受 -XX:MaxPermSize 限制。
小白:所以"方法区"是规范概念,"永久代"和"Metaspace"是不同版本 HotSpot 的具体实现?那第三个词 "Safepoint" 呢?我在 GC 日志里经常看到 Time To Safepoint 这个指标。
大师(在白板上画图):Safepoint 是 JVM 中的一个全局同步点。你可以把它想象成篮球比赛中的"暂停哨"——裁判一吹哨,所有运动员必须停下手里的动作。JVM 在执行某些需要全局一致性的操作时(比如 GC 的根扫描、偏向锁撤销),必须让所有线程都到达 Safepoint。如果某个线程一直在执行大循环或者 JNI 调用,迟迟不肯"停",就会导致 TTSP(Time To Safepoint)过高——这就是很多人遇到的"莫名卡顿"的根因。
小胖:那 JIT 呢?我听说 Java 是解释执行的,但后来能跑得和 C++ 一样快,是因为有"即时编译"?
大师:没错。HotSpot 的默认执行策略叫"分层编译"(Tiered Compilation)。代码刚加载时用解释器跑——启动快但执行慢;热点代码(被反复调用的方法、循环体)会先被 C1 编译器快速优化,再被 C2 编译器深度优化。这就好比一家餐厅,刚开始所有菜都是现做的(解释),后来发现"宫保鸡丁"每天点 500 次,干脆提前备料、优化流程(JIT),出菜速度自然就快了。
技术映射:Interpreter(解释器)↔ 现点现做,延迟低但吞吐差;C1 ↔ 半成品预处理,快速交付;C2 ↔ 中央厨房深度优化,极致效率但有启动成本。
小白:最后一个问题——最近面试经常被问"OpenJDK 和 Oracle JDK 有什么区别"。我们线上用的是 Temurin,这三者之间是什么关系?
大师:好,这个必须讲清楚。OpenJDK 是开源的 Java SE 参考实现,所有 Oracle JDK、Adoptium Temurin、Amazon Corretto、Alibaba Dragonwell 都是基于 OpenJDK 构建的发行版。区别在于:
- Oracle JDK:Oracle 的官方发行版,历史上包含了少量商业特性(如 Java Flight Recorder 曾需付费),但从 JDK 17 起已基本与 OpenJDK 对齐。差异主要在于许可证与支持策略。
- Adoptium Temurin:Eclipse 基金会维护的免费发行版,经过 TCK 认证,是目前生产环境使用最广泛的 OpenJDK 构建之一。
- Alibaba Dragonwell:阿里维护的 OpenJDK 发行版,集成了多租户、大堆调优等定制增强。
技术映射:OpenJDK ↔ Linux 内核,Oracle JDK / Temurin / Dragonwell ↔ Red Hat / Ubuntu / CentOS——同一个内核,不同发行商加了自己的编译参数和补丁。
3. 项目实战
3.1 环境准备
| 组件 | 版本 | 用途 |
|---|---|---|
| JDK | OpenJDK 21 (Temurin) | 运行示例代码 |
| IDE | IntelliJ IDEA / VS Code | 编写 Java 代码 |
| 画图工具 | draw.io / Excalidraw | 绘制架构图 |
| 文档工具 | Markdown / Confluence | 沉淀术语词典 |
安装 JDK:
# macOS
brew install openjdk@21
# Ubuntu/Debian
sudo apt-get install openjdk-21-jdk
# Windows
winget install EclipseAdoptium.Temurin.21.JDK
验证安装:
java -version
# openjdk version "21.0.5" 2024-10-15 LTS
# OpenJDK Runtime Environment Temurin-21.0.5+11 (build 21.0.5+11-LTS)
# OpenJDK 64-Bit Server VM Temurin-21.0.5+11 (build 21.0.5+11-LTS, mixed mode, sharing)
3.2 分步实现
步骤一:构建基础术语词典
目标:创建一份团队共享的 Markdown 文档,统一关键术语定义。
## JVM 核心术语表
| 术语 | 英文全称 | 一句话定义 | 类比 |
|------|----------|-----------|------|
| JDK | Java Development Kit | 开发工具包,包含 javac、java 等工具 | 厨房全套设备 |
| JRE | Java Runtime Environment | 运行时环境,仅能运行 Java 程序 | 只有微波炉的食堂 |
| JVM | Java Virtual Machine | 执行字节码的虚拟机 | 食堂阿姨(执行者) |
| Bytecode | - | .class 文件中 JVM 可执行的中间指令 | 食堂标准化菜谱 |
| Heap | - | 运行时存放对象实例的内存区域 | 餐桌(放菜的) |
| Metaspace | - | 存放类元数据的本地内存区域 | 厨房菜谱档案柜 |
| Oop | Ordinary Object Pointer | HotSpot 内部表示 Java 对象的数据结构 | 每道菜的"容器" |
| Klass | - | HotSpot 内部表示 Java 类的数据结构 | 每道菜的"配方" |
| Safepoint | - | JVM 全局同步点,所有线程必须在此停顿 | 篮球比赛暂停哨 |
| JIT | Just-In-Time Compiler | 运行时将热点字节码编译为本地机器码 | 中央厨房流水线 |
| GC Root | Garbage Collection Root | 垃圾回收的"根"引用集合 | 餐桌上的"在用餐具" |
步骤二:绘制 JVM 架构图
目标:用 draw.io 或 Excalidraw 绘制一张 JVM 整体架构图,包含以下层次:
┌─────────────────────────────────────────────────────────┐
│ Java 源代码 (.java) │
│ ↓ javac │
│ 字节码 (.class) │
└──────────────────────────┬──────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ JVM (HotSpot) │
│ │
│ ┌──────────────────┐ ┌─────────────────────────┐ │
│ │ 类加载子系统 │ │ 运行时数据区 │ │
│ │ ┌──────────────┐│ │ ┌───────────────────┐ │ │
│ │ │ Bootstrap ││ │ │ 堆 (Heap) │ │ │
│ │ │ Platform ││ │ │ ┌── Young ─────┐ │ │ │
│ │ │ Application ││ │ │ │ Eden│S0│S1 │ │ │ │
│ │ └──────────────┘│ │ │ ├── Old ───────┤ │ │ │
│ │ 加载→链接→初始化 │ │ │ │ Old Gen │ │ │ │
│ └──────────────────┘ │ │ └──────────────┘ │ │ │
│ │ │ Metaspace(本地) │ │ │
│ │ │ 程序计数器(PC) │ │ │
│ │ │ 虚拟机栈(Stack) │ │ │
│ │ │ 本地方法栈 │ │ │
│ │ └───────────────────┘ │ │
│ ┌──────────────────┐ ┌─────────────────────────┐ │
│ │ 执行引擎 │ │ 本地方法接口(JNI) │ │
│ │ 解释器 + JIT(C1/C2)│ │ (调用 C/C++ 本地库) │ │
│ │ GC 自动回收 │ │ │ │
│ └──────────────────┘ └─────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
每个模块标注对应的源码入口(例如:类加载 → src/hotspot/share/classfile/,堆 → src/hotspot/share/gc/shared/)。
步骤三:用 jcmd 验证术语
目标:用 jcmd 命令验证 JVM 中的核心术语对应关系。
// TermDemo.java
public class TermDemo {
private static final String MSG = "Hello JVM";
public static void main(String[] args) throws Exception {
System.out.println("PID: " + ProcessHandle.current().pid());
// 分配一些对象在堆上,以便观察
byte[][] data = new byte[10][];
for (int i = 0; i < 10; i++) {
data[i] = new byte[1024 * 1024]; // 1MB each
}
// 打印类信息,验证 Metaspace 中有该类
System.out.println("Class: " + TermDemo.class.getName());
System.out.println("ClassLoader: " + TermDemo.class.getClassLoader());
Thread.sleep(60000); // 保持进程存活以供观察
}
}
编译运行后,另开终端执行:
# 查看 VM 基本信息
jcmd <pid> VM.version
# 输出: OpenJDK 64-Bit Server VM version 21.0.5+11-LTS
# 查看堆内存使用(Heap)
jcmd <pid> GC.heap_info
# 输出: PSYoungGen total=... Eden=... Survivor=... Old=...
# 查看系统属性
jcmd <pid> VM.system_properties
# 可见 java.class.path、java.vm.name 等
# 查看元空间
jcmd <pid> VM.metaspace
# 输出: Usage: ... (class space)
# 查看已加载的类(验证 Klass 的存在)
jcmd <pid> VM.class_hierarchy | grep TermDemo
可能遇到的坑:
jcmd提示Connection refused:JDK 8 及之前版本需要启动时加-XX:+StartAttachListener,JDK 9+ 默认开启。如果进程 PID 输入错误,确认用jps -l查找。VM.metaspace命令在某些发行版不可用:该命令是 Oracle JDK 在 JDK 8 后加入的诊断命令,Temurin 完全兼容。- 容器环境中
jcmd需要与目标进程使用同一用户:root 运行jcmd可能无法 attach 非 root 的 Java 进程。
3.3 测试验证
运行以下脚本,验证术语理解是否到位:
#!/bin/bash
# 验证脚本:检查术语表中每个概念是否能在运行时找到对应证据
echo "=== 1. 验证 JVM ==="
java -version
echo ""
echo "=== 2. 验证 Compressed Oops ==="
java -XX:+PrintFlagsFinal -version 2>&1 | grep UseCompressedOops
echo ""
echo "=== 3. 验证 Metaspace ==="
java -XX:+PrintFlagsFinal -version 2>&1 | grep MetaspaceSize
echo ""
echo "=== 4. 验证分层编译(JIT) ==="
java -XX:+PrintFlagsFinal -version 2>&1 | grep TieredCompilation
echo ""
echo "=== 5. 验证 Safepoint 日志 ==="
java -Xlog:safepoint:file=safepoint.log -version 2>&1
cat safepoint.log | head -5
预期输出应显示 UseCompressedOops = true、MetaspaceSize 默认值、TieredCompilation = true 以及 Safepoint 日志内容。
4. 项目总结
4.1 优点与缺点
| 维度 | 优点 | 缺点 |
|---|---|---|
| 术语统一 | 消除团队沟通歧义,故障复盘效率提升 | 术语数量多(50+),新人记忆负担重,建议分批消化 |
| 架构全景 | JVM 架构图可一页讲清整个运行时原理 | 每个模块深入需要大量篇幅,本章仅为"地图总览" |
| 源码导航 | 术语与源码路径一一绑定,学以致用 | 源码量大(HotSpot 超百万行),初看容易迷失 |
| 工具验证 | jcmd 命令链可即时将抽象概念可视化 | 生产环境使用诊断命令需注意权限与性能影响 |
| 对比发行版 | OpenJDK/Temurin/Corretto 差异边界清晰 | 各发行版补丁策略不同,细节需查阅各发行版文档 |
| 对比技术 | OpenJDK HotSpot | GraalVM | OpenJ9 |
|---|---|---|---|
| 成熟度 | 极高(25+ 年) | 中(新兴) | 高(IBM 维护) |
| GC 选择 | Serial/Parallel/G1/ZGC/Shenandoah | 同 HotSpot + 自研 | 自有 GC(低内存占用) |
| JIT 策略 | C1 + C2 分层编译 | Graal JIT(Java 编写) | 自研 JIT |
| 云原生适应 | 中(虚拟线程后改善) | 高(AOT Native Image) | 高(低内存启动快) |
4.2 适用场景
- 团队新人 onboarding:作为入职第一周必读材料,建立 JVM 整体认知。
- 跨部门故障复盘的"共同语言":开发/运维/测试对齐术语定义,避免各说各话。
- 技术选型参考:理解 OpenJDK 发行版差异,选择适合公司环境的构建。
- 源码阅读入门:知道每个模块在源码树的大致位置,避免迷路。
- 面试准备:覆盖 JVM 架构面试题的 80% 核心要点。
不适用场景:
- 已有 5 年以上 JVM 调优经验的资深开发可跳过本章。
- 仅需"能跑 Java 就行"的纯业务 CRUD 开发可将本章作为参考速查。
4.3 注意事项
| 类型 | 详细说明 |
|---|---|
| 版本陷阱 | "永久代"仅适用于 JDK ≤7;JDK 8+ 统一使用 Metaspace。面试或文档中不要混用。 |
| 术语歧义 | "方法区"是规范术语,"Metaspace"是 HotSpot 实现术语。写周报时建议标注清楚。 |
| 安全边界 | 诊断命令(jcmd/jmap)在生产环境使用时,注意 FULL GC 的副作用——jmap -histo:live 会触发 Full GC。 |
| 配置兼容 | -XX:MaxPermSize 在 JDK 8+ 上会被忽略且报警告,应改为 -XX:MaxMetaspaceSize。 |
4.4 常见踩坑经验
案例 1:Metaspace OOM 误判为 Heap OOM
某支付服务频繁 Full GC 后 OOM,运维看日志关键词 OutOfMemoryError 就扩容堆内存。结果从 4G 扩到 8G 后依然 OOM。后来用 jcmd VM.metaspace 发现元空间用了 512MB 却被默认限制在约 128MB,原因是 Groovy 脚本热加载未配置类卸载。根因:未区分 Heap OOM 与 Metaspace OOM,两者在 GC 前行为完全不同。
案例 2:Safepoint 导致定时任务延迟
数据同步任务每隔 5 秒执行一次,但监控显示偶尔延迟超过 15 秒。加 -Xlog:safepoint* 后发现某线程在 JNI 调用中停留超过 10 秒,阻塞了所有线程到达 Safepoint。根因:JNI 调用期间线程不会主动检查 Safepoint,仅在返回 JVM 时才会响应。修复:缩短 JNI 调用批次,或在 JNI 层手动插入 JVM_ENTRY 检查点。
案例 3:发行版差异导致 JMX 不可用
测试环境用的 AdoptOpenJDK 8,生产环境用的 Oracle JDK 8。某 JMX 监控脚本只在一端正常工作——因为 Oracle JDK 8 的 JMX 默认启用了认证,而 AdoptOpenJDK 8 的默认策略不同。根因:不同发行版对 com.sun.management.jmxremote 相关参数的默认值可能略有差异。
4.5 思考题
-
进阶题:在 JDK 17 中,
-XX:+UseCompressedOops默认开启的条件是什么?如果堆大小超过 32GB,压缩指针会自动关闭——请设计一个实验,用 JOL(Java Object Layout)工具验证:堆内同一对象的引用在 32GB 以下和以上分别占用多少字节? -
实战题:公司的微服务默认 JVM 参数中有一行
-XX:MaxPermSize=256m,但线上运行的是 JDK 21。这个参数会生效吗?请用java -XX:+PrintFlagsFinal验证你的判断,并写一封邮件模板建议团队如何修正这个配置。
答案提示:思考题 1 答案见第 5 章;思考题 2 答案见本章"注意事项"表格。
4.6 跨部门阅读提示
| 角色 | 精读部分 | 选修部分 | 输出物 |
|---|---|---|---|
| 开发 | 术语词典、架构图、oop/Klass 概念 | 发行版对比 | 绘制并讲解架构图给组内同伴 |
| 运维 | 术语词典、jcmd 命令链、Safepoint 解释 | 发行版对比 | 在 Zabbix/Prometheus 中补齐 Metaspace 监控项 |
| 测试 | 术语词典、Metaspace 与 Heap OOM 区分 | 发行版差异 | 编写 OOM 场景测试用例,明确断言是 Heap 还是 Metaspace |
下一章预告:第 2 章将带大家深入 OpenJDK 源码目录,并在本机/容器中编译一份带 debug 符号的 JDK 镜像,为后续源码级调试打下基础。
延伸阅读与资源
Java 工程师进阶:从 JVM 生产排障到OpenJDK原理
Java 工程师进阶:从 JVM 生产排障到OpenJDK原理

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号