JVM 是 Java 生态的基石。理解其内存布局、类加载机制与执行引擎,不仅是性能调优的前提,更是排查 OOM、类冲突、类泄露等疑难杂症的必备技能。
1. JVM 运行时数据区总览
┌─────────────────────────────────────────────────────┐
│ JVM Runtime │
├─────────────────────────────────────────────────────┤
│ 线程私有(随线程生灭) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ PC 寄存器 │ │ 虚拟机栈 │ │ 本地方法栈 │ │
│ │ (计数指令) │ │ (Stack) │ │ (Native) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────┤
│ 线程共享(所有线程可见) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 堆 (Heap) │ │
│ │ ┌─────────────┐ ┌──────────────────────┐│ │
│ │ │ 年轻代 │ │ 老年代 ││ │
│ │ │ Eden / S0/S1│ │ (Old Gen) ││ │
│ │ └─────────────┘ └──────────────────────┘│ │
│ └─────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 元空间 (Metaspace, Java 8+) │ │
│ │ 类元数据 / 方法元数据 / 常量池 / 符号引用 │ │
│ └─────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────┤
│ 直接内存(堆外) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 直接堆外内存 (Direct Buffer / Unsafe.allocate) │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
1.1 各区域详解
| 区域 | 用途 | 异常 | 参数 |
|---|---|---|---|
| PC 寄存器 | 当前线程执行字节码行号 | 无(Native 方法时为 Undefined) | — |
| 虚拟机栈 | 方法调用栈帧(局部变量、操作数栈、动态链接、返回地址) | StackOverflowError / OutOfMemoryError | -Xss |
| 本地方法栈 | Native 方法调用 | 同上 | — |
| 堆 | 对象实例与数组 | OutOfMemoryError: Java heap space | -Xms / -Xmx / -Xmn |
| 元空间 | 类元数据(Java 8+ 替代永久代) | OutOfMemoryError: Metaspace | -XX:MaxMetaspaceSize |
| 直接内存 | NIO 缓冲区、Netty 等 | OutOfMemoryError: Direct buffer memory | -XX:MaxDirectMemorySize |
1.2 堆:对象分配与晋升
新对象 → Eden 区
├─ Minor GC 后存活 → Survivor 区 (S0 或 S1)
│ ├─ 交替复制到另一 Survivor
│ └─ 每经历一次 Minor GC,年龄 +1
│ └─ 年龄 ≥ -XX:MaxTenuringThreshold 或 Survivor 50% 同龄对象 → 晋升老年代
└─ 大对象(-XX:PretenureSizeThreshold) → 直接分配老年代
-XX:TargetSurvivorRatio = 50 (Survivor 目标使用率)
空间分配担保:Minor GC 前检查老年代最大连续空间是否大于新生代所有对象总空间;否则查看 HandlePromotionFailure。JDK 6u24 后此参数废弃,默认开启。
2. 对象内存布局
对象实例在堆中的结构(HotSpot,64 位,默认开启压缩指针):
┌─────────────────┐ 0 mark word (8 bytes)
│ 对象头 │ - 哈希码 / GC 年龄 / 锁状态
│ ───────────── │ - 类型指针 Klass pointer
│ Mark Word │ (压缩后 4 bytes / 64 位 8 bytes)
│ Klass Pointer │ (数组对象还有 4 bytes 数组长度)
├─────────────────┤
│ 实例数据 │ 字段值(父类字段在前,按对齐填充排序)
│ (Instance Data)│
├─────────────────┤
│ 对齐填充 │ 8 字节对齐
│ (Padding) │
└─────────────────┘
2.1 锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
对象创建 → 无锁(001)
→ 第一个线程获取 → 偏向锁(101)→ Mark Word 记录线程 ID
→ 出现另一个线程竞争 → 轻量级锁(00)→ CAS 自旋
→ 自旋超限(默认 10 次 / 自适应) → 重量级锁(10)→ Monitor
JVM 参数控制:
| 参数 | 说明 |
|---|---|
-XX:+UseBiasedLocking | 启用偏向锁(JDK 15+ 废弃) |
-XX:BiasedLockingStartupDelay=0 | 启动 0s 后启用 |
3. 类加载机制
3.1 类生命周期
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
↑______________________________________________|
各阶段职责:
| 阶段 | 内容 |
|---|---|
| 加载 | 读取 .class → 字节数组 → 生成 Class 对象 |
| 验证 | 格式/元数据/字节码/符号引用验证 |
| 准备 | 静态变量分配内存 + 零值初始化(final 直接赋值常量) |
| 解析 | 符号引用 → 直接引用(类、字段、方法、接口) |
| 初始化 | 执行 <clinit>()(静态变量赋值 + 静态代码块) |
| 卸载 | GC 回收(需满足:ClassLoader 无引用、Class 无实例、无反射引用) |
3.2 类加载器层级(双亲委派)
JVM 内建 应用层
┌────────────────────┐ ┌─────────────────────────────────┐
│ Bootstrap CL │ │ │
│ (C++ 实现) │ │ Application ClassLoader │
│ rt.jar / jmod │ │ classpath / jar │
│ ↑ │ │ ↑ │
│ Ext CL (Platform) │ │ Extension 模块 │
│ jre/lib/ext │◄─────┤ ↑ │
│ ↑ │ 委托 │ Custom ClassLoader │
│ App CL (System) │◄─────┤ Tomcat/Spring Boot Loader │
│ classpath │ │ │
└────────────────────┘ └─────────────────────────────────┘
委托顺序:Custom → App → Ext → Bootstrap
加载顺序:Bootstrap → Ext → App → Custom(先找到先加载)
双亲委派的意义:
- 安全:防止核心类被篡改(String、Object 等只能由 Bootstrap 加载)
- 避免重复加载:保证同一份字节码在 JVM 中只有一个 Class 实例
3.3 打破双亲委派
场景 1:SPI 机制(JDBC、JNDI)
// ServiceLoader 由 Bootstrap ClassLoader 调用
// 但需要加载厂商实现(如 MySQL driver)在 classpath
// 解决方案:Thread Context ClassLoader (TCCL)
Class.forName("com.mysql.cj.jdbc.Driver", true, Thread.currentThread().getContextClassLoader());
场景 2:Tomcat 隔离
- 每个 Web 应用独立 WebAppClassLoader
- 优先加载
WEB-INF/classes和WEB-INF/lib,不向上委托 common/lib共享类由 CommonClassLoader 加载
场景 3:OSGi / 模块化
- 每个 Bundle 独立 ClassLoader
- 复杂的导入/导出依赖图
3.4 自定义 ClassLoader
public class CustomClassLoader extends ClassLoader {
private final String basePath;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = loadClassData(name);
return defineClass(name, bytes, 0, bytes.length);
}
// 打破双亲委派:先自己加载,找不到再委托
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("com.myapp.plugin")) {
return findClass(name);
}
return super.loadClass(name, resolve);
}
}
4. Java 内存模型(JMM)
4.1 主内存与工作内存
主内存(Main Memory)←→ 工作内存(线程私有)
┃
┌─────────────┼─────────────┐
↓ ↓ ↓
线程 A 工作内存 线程 B 工作内存 线程 C 工作内存
规定:所有变量存储在主内存;每条线程有自己的工作内存,保存了被线程使用到的变量的副本。线程对变量的操作必须在工作内存中进行。
4.2 happens-before 规则
无需同步即可保证有序性的场景:
- 程序次序规则:同一线程内,书写在前面的操作 happens-before 后面的
- 锁定规则:解锁 happens-before 后续对同锁的加锁
- volatile 规则:写 happens-before 后续读
- 传递性:A → B, B → C ⇒ A → C
- 线程启动:
Thread.start()happens-before 线程内操作 - 线程终止:线程内操作 happens-before 检测终止(
Thread.join()/isAlive()) - 中断规则:
interrupt()happens-before 检测到中断 - 对象终结:构造函数执行 happens-before
finalize()
4.3 volatile 语义
| 语义 | 实现 |
|---|---|
| 可见性 | 写操作立刻刷回主内存;读操作从主内存刷新 |
| 禁止重排序 | 插入内存屏障,阻止指令重排 |
private volatile boolean flag = false;
// 线程 A
flag = true; // StoreStore + StoreLoad 屏障
// 线程 B
while (!flag) {} // LoadLoad + LoadStore 屏障
// flag 为 true → 能看到线程 A 之前的所有写
volatile 不保证原子性:i++ 需配合 synchronized 或 AtomicInteger。
5. 字节码执行引擎
5.1 运行时栈帧结构
局部变量表 (Local Variable Table)
┌── Slot 0: this (实例方法) / 静态方法跳过
├── Slot 1: 参数/局部变量 1
├── Slot 2: 参数/局部变量 2 (long/double 占 2 slots)
└── ...
操作数栈 (Operand Stack)
┌── 栈顶
├── 操作数
└── 栈底
动态链接 (Dynamic Linking): 指向运行时常量池的方法引用
返回地址 (Return Address): 方法出口,异常处理表
5.2 方法调用指令
| 指令 | 绑定时机 | 类型 |
|---|---|---|
invokestatic | 编译期 | 静态方法 |
invokespecial | 编译期 | 构造器/private/super |
invokevirtual | 运行时 | 虚方法(动态分派) |
invokeinterface | 运行时 | 接口方法 |
invokedynamic | 运行时 | Lambda、反射(引导方法确定) |
5.3 动态类型语言支持(invokedynamic)
Java 8 的 Lambda 表达式并非语法糖,而是利用 invokedynamic + LambdaMetafactory.metafactory() 在运行时生成调用点。
// 源码
Supplier<String> s = () -> "hello";
// 字节码(简化)
invokedynamic #0:get:()Ljava/util/function/Supplier;
// 运行时:LambdaMetafactory 生成适配类
// class Lambda$1 implements Supplier { String get() { return "hello"; } }
6. 实用工具
| 工具 | 用途 |
|---|---|
javap -c -v ClassName | 反编译字节码 |
javap -constants ClassName | 查看常量池 |
jclasslib | GUI 字节码查看器 |
HSDB (HotSpot Debugger) | 查看对象头、Klass 结构 |
7. JVM 参数速查
# 内存区域
-Xms512m -Xmx2g -Xmn768m
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=256m
# GC 打印
-XX:+PrintGCDetails # JDK 8
-Xlog:gc*:file=gc.log:time,uptime:filecount=10,filesize=100m # JDK 9+
# 调试
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof
-XX:OnOutOfMemoryError="kill -9 %p"
# 类加载
-XX:+TraceClassLoading
-verbose:class
延伸阅读
- JVM 性能调优与 GC 优化 — 深入到垃圾回收器选型与参数调优
- Java 高并发编程精要 — JMM 与并发原语的实战应用
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。