运行时数据区
JVM 运行时数据区是 Java 程序运行时的内存区域,分为线程私有和线程共享两部分。
整体架构
┌─────────────────────────────────────────────────────────────┐
│ JVM 运行时数据区 │
├─────────────┬─────────────┬─────────────┬─────────────────┤
│ 程序计数器 │ 虚拟机栈 │ 本地方法栈 │ 堆 │
│ (线程私有) │ (线程私有) │ (线程私有) │ (线程共享) │
├─────────────┴─────────────┴─────────────┼─────────────────┤
│ 方法区 │ 运行时常量池 │
│ (线程共享) │ (方法区一部分) │
└─────────────────────────────────────────┴─────────────────┘一、程序计数器 (Program Counter)
概述
程序计数器是一块较小的内存空间,记录当前线程执行的字节码行号。
特点
| 特性 | 说明 |
|---|---|
| 线程私有 | 每个线程都有独立的 PC |
| 无 OOM | 唯一没有 OutOfMemoryError 的区域 |
| 存储内容 | 字节码指令地址 / undefined(本地方法) |
工作原理
// Java 代码
int a = 1;
int b = 2;
int c = a + b;
// 字节码
0: iconst_1 // PC = 0
1: istore_1 // PC = 1
2: iconst_2 // PC = 2
3: istore_2 // PC = 3
4: iload_1 // PC = 4
5: iload_2 // PC = 5
6: iadd // PC = 6
7: istore_3 // PC = 7为什么需要 PC?
多线程环境下,CPU 需要切换线程。PC 记录每个线程的执行位置,恢复时继续执行。
二、虚拟机栈 (Java Virtual Machine Stack)
概述
虚拟机栈描述 Java 方法执行的内存模型:每个方法执行时创建一个栈帧。
栈帧结构
┌────────────────────────┐
│ 栈帧 │
├────────────────────────┤
│ 局部变量表 │
│ (Local Variables) │
├────────────────────────┤
│ 操作数栈 │
│ (Operand Stack) │
├────────────────────────┤
│ 动态链接 │
│ (Dynamic Linking) │
├────────────────────────┤
│ 方法返回地址 │
│ (Return Address) │
└────────────────────────┘局部变量表
存储方法参数和局部变量:
public int add(int a, int b) {
int c = a + b;
return c;
}
// 局部变量表
// slot 0: this(非静态方法)
// slot 1: a
// slot 2: b
// slot 3: c注意:局部变量表没有初始化值,必须显式初始化后才能使用。
操作数栈
方法执行过程中的计算工作区:
int c = a + b;
// 字节码执行过程
iload_1 // 将 a 压入操作数栈
iload_2 // 将 b 压入操作数栈
iadd // 弹出两个值相加,结果压栈
istore_3 // 弹出结果存入局部变量表异常
| 异常 | 触发条件 |
|---|---|
| StackOverflowError | 栈深度超过限制 |
| OutOfMemoryError | 栈扩展时内存不足 |
栈配置
# 设置每个线程栈大小
-Xss256k三、本地方法栈 (Native Method Stack)
概述
为 Native 方法服务的栈,与虚拟机栈类似。
特点
- 调用 C/C++ 实现的本地方法
- HotSpot 中与虚拟机栈合二为一
- 同样可能抛出 StackOverflowError 和 OutOfMemoryError
示例
// Object.hashCode() 是本地方法
public native int hashCode();
// Thread.start() 调用本地方法
private native void start0();四、堆 (Heap)
概述
堆是 JVM 最大的内存区域,存储所有对象实例和数组。
特点
| 特性 | 说明 |
|---|---|
| 线程共享 | 所有线程共享堆内存 |
| GC 主战场 | 垃圾收集的主要区域 |
| 可扩展 | -Xms 和 -Xmx 控制 |
堆内存结构
┌─────────────────────────────────────────────┐
│ 堆 (Heap) │
├─────────────────────┬───────────────────────┤
│ 新生代 (Young) │ 老年代 (Old) │
├─────────┬─────────┬─┴───────────────────────┤
│ Eden │ Survivor │ │
│ │ S0 S1 │ │
│ 8 : 1 : 1 │ │
└─────────┴─────────┴─────────────────────────┘对象分配流程
新对象
↓
Eden 区分配 → 空间足够?
↓ 否
触发 Minor GC
↓
存活对象 → Survivor 区 → 年龄 +1
↓
年龄 ≥ 15 (默认) → 晋升老年代大对象直接进老年代
// 大对象:需要大量连续内存的对象(长数组、大字符串)
// -XX:PretenureSizeThreshold 设置阈值
byte[] bigArray = new byte[4 * 1024 * 1024]; // 4MB,可能直接进老年代堆配置
# 初始堆大小
-Xms512m
# 最大堆大小
-Xmx1024m
# 新生代比例
-XX:NewRatio=2 # 老年代:新生代 = 2:1
# Eden:Survivor 比例
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1OOM 场景
// 堆溢出
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 持续创建大对象
}
// java.lang.OutOfMemoryError: Java heap space五、方法区 (Method Area)
概述
存储类信息、常量、静态变量、即时编译器生成的代码等。
特点
| 特性 | 说明 |
|---|---|
| 线程共享 | 所有线程共享 |
| 逻辑上是堆的一部分 | HotSpot 中称为"非堆" |
| 不同实现 | JDK7: 永久代 / JDK8+: 元空间 |
永久代 vs 元空间
| 特性 | 永久代 (JDK7-) | 元空间 (JDK8+) |
|---|---|---|
| 位置 | JVM 堆内存 | 本地内存 |
| 大小限制 | 受堆大小限制 | 受系统内存限制 |
| OOM | PermGen space | Metaspace |
| 配置 | -XX:PermSize | -XX:MetaspaceSize |
存储内容
方法区
├── 类信息
│ ├── 类名、访问修饰符
│ ├── 父类、接口
│ ├── 字段、方法
│ └── 方法字节码
├── 运行时常量池
├── 静态变量
└── 即时编译代码方法区配置
# JDK8+ 元空间
-XX:MetaspaceSize=128m # 初始大小
-XX:MaxMetaspaceSize=256m # 最大大小六、运行时常量池 (Runtime Constant Pool)
概述
运行时常量池是方法区的一部分,存储编译期生成的字面量和符号引用。
常量池内容
| 类型 | 示例 |
|---|---|
| 字面量 | 数字、字符串 |
| 符号引用 | 类/方法/字段的引用 |
String 常量池
// 字符串常量池示例
String s1 = "hello"; // 常量池
String s2 = "hello"; // 复用常量池中的对象
String s3 = new String("hello"); // 堆中新建对象
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s1 == s3.intern()); // true
// intern() 方法:将字符串加入常量池并返回引用常量池溢出
// JDK6 会 OOM
// JDK7+ 常量池在堆中,受堆大小限制
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}七、直接内存 (Direct Memory)
概述
直接内存不是 JVM 运行时数据区的一部分,而是 NIO 使用的本地内存。
特点
| 特性 | 说明 |
|---|---|
| 非堆内存 | 直接使用系统内存 |
| 零拷贝 | 避免 Java 堆与 Native 堆之间复制 |
| 受系统限制 | 可能导致 OOM |
NIO 示例
// 使用直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// 底层使用 Unsafe 分配本地内存
// 访问更快,但分配/释放开销更大配置
# 最大直接内存
-XX:MaxDirectMemorySize=256m八、对象内存布局
对象结构
┌────────────────────────────────────┐
│ 对象头 (Header) │
├────────────────────────────────────┤
│ Mark Word (8 bytes) │
│ - 哈希码、锁状态、GC 年龄 │
├────────────────────────────────────┤
│ 类型指针 (4/8 bytes) │
│ - 指向类元数据 │
├────────────────────────────────────┤
│ 实例数据 (Instance Data) │
│ - 字段值 │
├────────────────────────────────────┤
│ 对齐填充 (Padding) │
│ - 保证 8 字节对齐 │
└────────────────────────────────────┘对象大小计算
// 64 位 JVM,开启指针压缩
Object obj = new Object(); // 16 字节
// Mark Word: 8 字节
// 类型指针: 4 字节(压缩后)
// 对齐填充: 4 字节
// 数组对象额外 4 字节存储长度
int[] arr = new int[10]; // 16 + 40 = 56 字节九、内存模型对比
| 区域 | 线程安全 | OOM 类型 | 存储内容 |
|---|---|---|---|
| 程序计数器 | 私有 | 无 | 字节码行号 |
| 虚拟机栈 | 私有 | StackOverflow / OOM | 栈帧 |
| 本地方法栈 | 私有 | StackOverflow / OOM | Native 栈帧 |
| 堆 | 共享 | OOM | 对象实例 |
| 方法区 | 共享 | OOM | 类信息、常量 |
| 直接内存 | 共享 | OOM | NIO 缓冲区 |
常见面试题
Q1: 栈和堆的区别?
| 特性 | 栈 | 堆 |
|---|---|---|
| 存储内容 | 基本类型、对象引用 | 对象实例 |
| 生命周期 | 方法结束自动释放 | GC 回收 |
| 空间大小 | 较小,固定 | 较大,可扩展 |
| 碎片 | 无 | 有 |
Q2: 为什么 String 要放在常量池?
- 节省内存:相同字符串只存一份
- 性能优化:常量池查找比堆快
- 线程安全:常量池字符串不可变
Q3: 方法区会 OOM 吗?
会。动态生成大量类(如 CGLib、动态代理)可能撑爆方法区。
// CGLib 动态代理生成大量类
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> null);
enhancer.create(); // 不断生成新类
}
// java.lang.OutOfMemoryError: MetaspaceQ4: 如何排查 OOM?
# 1. 打印 GC 日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
# 2. OOM 时生成堆转储
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=dump.hprof
# 3. 使用工具分析
jmap -histo:live <pid> # 查看对象统计
jhat dump.hprof # 分析堆转储
MAT / VisualVM # 可视化工具小结
- 程序计数器:线程私有,记录执行位置
- 虚拟机栈:存储栈帧,方法调用链
- 本地方法栈:Native 方法调用
- 堆:对象实例存储,GC 主战场
- 方法区:类信息、常量、静态变量
- 运行时常量池:字面量和符号引用
- 直接内存:NIO 使用的本地内存