# JVM内存模型详解,堆栈方法区到底怎么分的

0 阅读3分钟

每次说"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动态类太多
直接内存OutOfMemoryErrorNIO缓冲区分配过多

排查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看看自己项目的堆内存分布,有个直观感受。