面试必备:JVM 内存模型(JMM)详解
JVM 内存区域全景图
┌─────────────────────────────────────────────────────────────────┐
│ JVM 进程 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Method Area │ │
│ │ (方法区/元空间) │ │
│ │ • 类信息 │ │
│ │ • 运行时常量池 │ │
│ │ • 静态变量 │ │
│ │ • JIT 编译后的代码 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────┐ ┌─────────────────────────┐ │
│ │ Heap (堆) │ │ Native Method Stack │ │
│ │ • 对象实例 │ │ (本地方法栈) │ │
│ │ • 数组 │ │ • native 方法 │ │
│ │ │ │ │ │
│ │ ┌─────────────┐ │ └─────────────────────────┘ │
│ │ │ Eden │ │ │
│ │ │ Survivor S0 │ │ ┌─────────────────────────┐ │
│ │ │ Survivor S1 │ │ │ PC Register │ │
│ │ │ Old Gen │ │ │ (程序计数器) │ │
│ │ └─────────────┘ │ │ 当前执行的行号 │ │
│ └───────────────────┘ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ JVM Stack │ │
│ │ (虚拟机栈) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Thread 1 │ │ Thread 2 │ │ Thread 3 │ │ │
│ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │
│ │ │ │Frame │ │ │ │Frame │ │ │ │Frame │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │局部变量│ │ │ │局部变量│ │ │ │局部变量│ │ │ │
│ │ │ │操作数栈│ │ │ │操作数栈│ │ │ │操作数栈│ │ │ │
│ │ │ │动态链接│ │ │ │动态链接│ │ │ │动态链接│ │ │ │
│ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
线程私有区域(每个线程独享)
1. 程序计数器(PC Register)
当前执行的字节码行号
作用:记录当前线程执行到哪一行字节码
特点:
- 线程私有
- 唯一不会 OutOfMemoryError 的区域
- 如果执行 native 方法,计数器为空
2. 虚拟机栈(JVM Stack)
public void methodA() {
int a = 1; // 栈帧1 入栈
methodB(); // 栈帧2 入栈
}
public int methodB() {
int b = 2; // 执行中
return b;
}
栈帧结构:
┌─────────────────────┐
│ 栈帧 │
├─────────────────────┤
│ 局部变量表 │ // local variables
│ • 参数 │
│ • 局部变量 │
├─────────────────────┤
│ 操作数栈 │ // operand stack
│ • 临时操作空间 │
├─────────────────────┤
│ 动态链接 │ // dynamic linking
│ • 指向常量池 │
├─────────────────────┤
│ 返回地址 │ // return address
│ • 方法出口 │
└─────────────────────┘
异常:
StackOverflowError:栈深度过深(常见递归没终止)OutOfMemoryError:栈内存不够(可以动态扩展时)
3. 本地方法栈(Native Method Stack)
和虚拟机栈类似,但服务于 native 方法(C/C++ 实现)。
线程共享区域
4. 堆(Heap)
核心区域,几乎所有对象实例和数组都存在这里。
// 堆中分配
Object obj = new Object(); // 对象实例在堆中
堆内存划分(Java 8+)
┌──────────────────────────────────────────────┐
│ 堆内存 │
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ Young Gen │ │ Old Gen │ │
│ │ (新生代) │ │ (老年代) │ │
│ │ │ │ │ │
│ │ ┌──────────┐ │ │ │ │
│ │ │ Eden │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ ├──────────┤ │ │ │ │
│ │ │ Survivor │ │ │ │ │
│ │ │ S0 │ │ │ │ │
│ │ ├──────────┤ │ │ │ │
│ │ │ Survivor │ │ │ │ │
│ │ │ S1 │ │ │ │ │
│ │ └──────────┘ │ │ │ │
│ │ │ │ │ │
│ │ 比例:8:1:1 │ │ 比例:2:1 │ │
│ └────────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐│
│ │ Metaspace (元空间) ││
│ │ • 类信息 ││
│ │ • 方法字节码 ││
│ └──────────────────────────────────────────┘│
└──────────────────────────────────────────────┘
| 区域 | 比例 | 说明 |
|---|---|---|
| Eden | 80% | 新对象分配 |
| Survivor S0 | 10% | 存活对象交换 |
| Survivor S1 | 10% | 存活对象交换 |
| Old Gen | 2/3 | 长寿对象、大对象 |
| Metaspace | - | 类信息(不在堆中) |
5. 方法区(Method Area)
存储类信息、运行时常量池、静态变量、JIT 编译后的代码。
JDK 7:方法区在堆中(PermGen 永久代)
JDK 8+:移出堆外(Metaspace 元空间)
// 方法区存储
public class User {
static int count = 0; // 静态变量 → 方法区
static final int MAX = 100; // 常量 → 运行时常量池
public void say() { } // 方法字节码
}
对象创建流程
new Object()
│
▼
① 类加载检查
│
▼
② 分配内存
│
├── 指针碰撞(内存规整)
│ └─ 适用于 Serial、ParNew 等收集器
│
└── 空闲列表(内存碎片)
└─ 适用于 CMS 等收集器
│
▼
③ 初始化零值
│
▼
④ 设置对象头
│
▼
⑤ 执行构造函数
常见内存溢出
1. 堆溢出(OutOfMemoryError: Java heap space)
// 创建大量对象不释放
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 不断添加 1MB
}
2. 栈溢出(StackOverflowError)
// 无限递归
public void recursion() {
recursion(); // 没有终止条件
}
3. 方法区溢出
// 动态生成大量类
// 使用 CGLIB 或反射不断创建类
4. 元空间溢出(Metaspace)
// JDK 8+,类加载过多
// 常见于框架动态代理、字节码生成
内存参数配置
# 堆大小配置
-Xms256m # 初始堆大小
-Xmx512m # 最大堆大小
-Xmn128m # 新生代大小
-XX:MetaspaceSize=128m # 元空间初始大小
-XX:MaxMetaspaceSize=256m
# 打印 GC 日志
-XX:+PrintGCDetails
-Xloggc:gc.log
# OOM 时导出堆 dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
面试高频问题
Q1:什么情况下对象会进入老年代?
- 年龄达到阈值(默认 15,CMS 是 6)
- 大对象直接分配(超过 Eden 区)
- 动态年龄判断:Survivor 中相同年龄所有对象之和 > Survivor 的 50%
Q2:为什么需要两个 Survivor 区?
只有一个 Survivor 区会产生内存碎片。两个 Survivor(S0、S1)交替使用,避免碎片化,方便复制算法。
Q3:方法区和堆的区别?
- 堆:对象实例、数组,线程共享
- 方法区:类信息、静态变量、常量池,线程共享
- JDK 8+:方法区移到元空间,不在堆中
总结
| 区域 | 线程私有/共享 | 异常 |
|---|---|---|
| 程序计数器 | 私有 | 无 |
| 虚拟机栈 | 私有 | SOE, OOM |
| 本地方法栈 | 私有 | SOE, OOM |
| 堆 | 共享 | OOM |
| 元空间 | 共享 | OOM |
核心理解:堆是对象的大本营,栈是方法的执行轨迹,方法区是类的档案室。