JVM 内存模型与区域划分
运行时数据区
JVM 在运行 Java 程序时,会将内存划分为多个区域,各有不同的用途和生命周期。
┌─────────────────────────────────────────────────────────┐
│ JVM 运行时数据区 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 方法区 │ │ 堆 │ ← 线程共享 │
│ │ (Method Area) │ │ (Heap) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 虚拟机栈 │ │ 本地方法栈 │ │ 程序计数器 │ ← 线程私有 │
│ │ (Stack) │ │(Native) │ │ (PC) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────┘线程私有区域
1. 程序计数器(Program Counter Register)
- 记录当前线程正在执行的字节码指令地址
- 唯一不会发生 OutOfMemoryError 的区域
- 如果执行的是 Native 方法,计数器值为空(Undefined)
2. 虚拟机栈(Java Virtual Machine Stack)
每个方法调用会创建一个栈帧,包含:
┌─────────────────────────────┐
│ 栈帧 │
├─────────────────────────────┤
│ 局部变量表 (Local Variables) │ ← 存储方法参数和局部变量
├─────────────────────────────┤
│ 操作数栈 (Operand Stack) │ ← 存储计算过程中的中间结果
├─────────────────────────────┤
│ 动态链接 (Dynamic Linking) │ ← 运行时常量池的引用
├─────────────────────────────┤
│ 方法返回地址 │ ← 方法返回后继续执行的位置
└─────────────────────────────┘可能的异常:
StackOverflowError:栈深度超过限制(如递归过深)OutOfMemoryError:无法申请足够的栈内存
java
/**
* 栈溢出示例
* @author yjhu
*/
public class StackOverflowDemo {
private int depth = 0;
public void recursion() {
depth++;
recursion(); // 无限递归
}
public static void main(String[] args) {
StackOverflowDemo demo = new StackOverflowDemo();
try {
demo.recursion();
} catch (StackOverflowError e) {
System.out.println("栈深度: " + demo.depth);
// 默认栈大小下,通常 10000+ 次调用就会溢出
}
}
}3. 本地方法栈(Native Method Stack)
与虚拟机栈类似,但服务于 Native 方法(如 C/C++ 实现的方法)。
线程共享区域
1. 堆(Heap)
Java 内存管理的核心区域,几乎所有对象实例都在堆上分配。
┌─────────────────────────────────────────────────────────┐
│ 堆 (Heap) │
├──────────────────────────────┬──────────────────────────┤
│ 年轻代 (Young Gen) │ 老年代 (Old Gen) │
├────────┬────────┬────────────┤ │
│ Eden │ S0 │ S1 │ │
│ 区 │(From) │ (To) │ │
│ 80% │ 10% │ 10% │ │
└────────┴────────┴────────────┴──────────────────────────┘对象分配流程:
- 新对象优先在 Eden 区分配
- Eden 满了触发 Minor GC,存活对象移到 Survivor 区
- 对象在 Survivor 区每熬过一次 GC,年龄 +1
- 年龄达到阈值(默认 15)晋升到老年代
- 老年代满了触发 Major GC / Full GC
2. 方法区(Method Area)
存储类的元信息、常量、静态变量等。
JDK 版本变化:
| 版本 | 实现方式 | 特点 |
|---|---|---|
| JDK 7 及之前 | 永久代(PermGen) | 使用堆内存,容易 OOM |
| JDK 8 及之后 | 元空间(Metaspace) | 使用本地内存,默认不限大小 |
bash
# JDK 7 设置永久代大小
-XX:PermSize=256m -XX:MaxPermSize=512m
# JDK 8+ 设置元空间大小
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m直接内存(Direct Memory)
不属于 JVM 运行时数据区,但使用 NIO 时会用到。
java
// 分配直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);优点:减少数据在 JVM 堆和系统内存之间的复制 缺点:分配和回收成本较高
内存参数设置
bash
# 堆内存
-Xms512m # 初始堆大小
-Xmx2g # 最大堆大小
-Xmn256m # 年轻代大小
# 栈内存
-Xss256k # 每个线程的栈大小
# 元空间
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
# 直接内存
-XX:MaxDirectMemorySize=256m常见 OOM 场景
| 区域 | 异常信息 | 常见原因 |
|---|---|---|
| 堆 | Java heap space | 对象过多、内存泄漏 |
| 元空间 | Metaspace | 动态生成大量类(如 CGLib) |
| 栈 | StackOverflowError | 递归过深 |
| 直接内存 | Direct buffer memory | NIO 使用不当 |
