每次说"Java内存模型",可能指两个东西:一个是JMM(Java Memory Model,并发相关),另一个是JVM的运行时内存区域划分。
今天聊的是后者——JVM运行时数据区。面试必问,理解了这些才能看懂OOM、内存泄漏这些问题。
JVM运行时数据区全景
JVM运行时数据区
├── 线程私有
│ ├── 程序计数器(PC Register)
│ ├── 虚拟机栈(VM Stack)
│ └── 本地方法栈(Native Method Stack)
└── 线程共享
├── 堆(Heap)
└── 方法区(Method Area / 元空间)
程序计数器:当前执行到哪了
每个线程都有自己的程序计数器,记录当前线程执行到了哪一行字节码。
- 执行Java方法时:记录正在执行的字节码指令地址
- 执行Native方法时:未定义(Undefined)
- 唯一不会发生OOM的区域
虚拟机栈:方法调用的记录
每个方法被调用时,JVM会在虚拟机栈中创建一个栈帧:
虚拟机栈
┌─────────────────┐ ← 栈顶(当前方法)
│ 栈帧:methodC() │
│ 局部变量表 │
│ 操作数栈 │
│ 动态链接 │
│ 返回地址 │
├─────────────────┤
│ 栈帧:methodB() │
├─────────────────┤
│ 栈帧:methodA() │
└─────────────────┘ ← 栈底
每个栈帧包含:
| 组成部分 | 作用 |
|---|---|
| 局部变量表 | 方法参数 + 方法内定义的局部变量 |
| 操作数栈 | 字节码指令的工作区 |
| 动态链接 | 指向方法区中该方法的引用 |
| 返回地址 | 方法结束后回到哪里继续执行 |
StackOverflowError:方法调用太深(比如无限递归),栈帧太多,栈空间不够了。
// 经典StackOverflow
public void recursion() {
recursion(); // 每次调用都压一个栈帧
}
本地方法栈
和虚拟机栈类似,只不过服务于Native方法(用C/C++实现的方法)。HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一了。
堆:对象都在这
堆是JVM管理的最大一块内存,几乎所有对象实例都在这里分配。
堆内存结构
┌─────────────────────────────────┐
│ 老年代 │
│ (长期存活的对象、大对象) │
├─────────────────────────────────┤
│ Survivor To │
├─────────────────────────────────┤
│ Survivor From │
├─────────────────────────────────┤
│ Eden │
│ (新创建的对象) │
└─────────────────────────────────┘
- 年轻代:Eden + 两个Survivor,新对象在这里分配
- 老年代:经过多次GC还存活的对象晋升到这里
- OutOfMemoryError:堆内存不够了,对象太多或内存泄漏
# 设置堆大小
java -Xms512m -Xmx2048m MyApp
# -Xms:初始堆大小
# -Xmx:最大堆大小
方法区(元空间)
方法区存储的是类的元数据——类结构、方法信息、常量池、静态变量等。
JDK 8之前,方法区的实现叫"永久代"(PermGen),用的是JVM自己管理的内存。
JDK 8开始改叫"元空间"(Metaspace),用的是操作系统的本地内存。
# JDK 8+ 设置元空间大小
java -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m MyApp
| 对比 | 永久代(JDK 7-) | 元空间(JDK 8+) |
|---|---|---|
| 内存位置 | JVM堆内 | 本地内存(操作系统) |
| 大小限制 | 受堆大小限制 | 理论上只受系统内存限制 |
| OOM风险 | 容易 | 较少 |
| GC效率 | 低 | 高 |
直接内存
NIO(New IO)中用到的DirectByteBuffer,分配的不是堆内存,而是直接内存(堆外内存)。
// 分配直接内存(不受-Xmx限制)
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
直接内存的好处是避免了堆内存和 native 内存之间的数据拷贝,IO性能更好。但不受JVM直接管理,需要手动释放。
各区域OOM总结
| 区域 | 异常类型 | 常见原因 |
|---|---|---|
| 虚拟机栈 | StackOverflowError | 递归太深 |
| 堆 | OutOfMemoryError | 对象太多/内存泄漏 |
| 方法区 | OutOfMemoryError | 加载类太多/CGLib动态类太多 |
| 直接内存 | OutOfMemoryError | NIO缓冲区分配过多 |
排查OOM的基本思路
# 1. 启动时加上OOM时自动dump
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof MyApp
# 2. 手动dump
jmap -dump:format=b,file=heap.hprof <pid>
# 3. 用MAT或VisualVM分析dump文件
# 找出占用内存最大的对象类型
JVM内存模型是理解GC、OOM、性能调优的基础。搞清楚每个区域存什么、什么时候会出问题,排查线上问题就有了方向。
建议用jmap -heap看看自己项目的堆内存分布,有个直观感受。