JVM内存区域划分与作用

JVM内存区域划分与作用

JVM 内存布局是排查线上问题的基础。很多开发者遇到 OutOfMemoryError 时不知道从哪里入手。本文结合实际案例,讲清楚各区域的作用和常见异常,帮你建立排查思路。

运行时数据区概览

┌─────────────────────────────────────────┐
│ 线程私有区域 │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │程序计数器 │ │虚拟机栈 │ │本地方法栈│ │
│ └──────────┘ └──────────┘ └─────────┘ │
├─────────────────────────────────────────┤
│ 线程共享区域 │
│ ┌───────────────────────────────────┐ │
│ │ 堆 │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 年轻代 │ │ 老年代 │ │ │
│ │ │ Eden │ │ │ │ │
│ │ │ Survivor0 │ │ │ │ │
│ │ │ Survivor1 │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ 方法区(元空间) │ │
│ │ 类信息、常量、静态变量、JIT代码 │ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ 直接内存(堆外) │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘

1. 程序计数器(Program Counter Register)

作用:当前线程执行的字节码行号指示器。

特点

  • 线程私有,每个线程独立
  • 唯一不会发生 OOM 的区域
  • 执行 Java 方法时记录字节码地址,执行 Native 方法时为空
线程A的程序计数器: 0x0001 (执行到第1行)
线程B的程序计数器: 0x0010 (执行到第16行)

2. 虚拟机栈(VM Stack)

作用:描述 Java 方法执行的内存模型,每个方法执行时创建栈帧。

#

栈帧结构

┌─────────────────┐
│ 局部变量表 │ 存放基本类型和对象引用
├─────────────────┤
│ 操作数栈 │ 方法执行的工作区
├─────────────────┤
│ 动态链接 │ 指向运行时常量池的方法引用
├─────────────────┤
│ 方法返回地址 │ 方法执行完毕后的返回位置
├─────────────────┤
│ 附加信息 │ 调试信息等
└─────────────────┘

#

局部变量表

public void method(int a, int b) {
int c = a + b; // 局部变量表: [a, b, c]
Object obj = new Object(); // 引用类型占1个slot
long d = 100L; // long/double占2个slot
}

#

常见问题:StackOverflowError

public void recursive() {
recursive(); // 无限递归,栈溢出
}

解决

  • 检查递归终止条件
  • 增加栈大小:-Xss512k

3. 本地方法栈(Native Method Stack)

作用:为 Native 方法服务。

特点

  • 线程私有
  • HotSpot 中与虚拟机栈合二为一
  • 也会抛出 StackOverflowError 和 OOM

4. 堆(Heap)

作用:存放对象实例和数组,是 JVM 管理的最大内存区域。

#

堆内存划分

┌──────────────────────────────────────┐
│ 堆 │
│ ┌────────────────────────────────┐ │
│ │ 年轻代 (Young) │ │ -Xmn
│ │ ┌──────────────────────────┐ │ │
│ │ │ Eden (8/10) │ │ │
│ │ ├──────────────────────────┤ │ │
│ │ │ Survivor0 (1/10) │ │ │
│ │ ├──────────────────────────┤ │ │
│ │ │ Survivor1 (1/10) │ │ │
│ │ └──────────────────────────┘ │ │
│ └────────────────────────────────┘ │
│ ┌────────────────────────────────┐ │
│ │ 老年代 (Old) │ │ -Xms - Xmn
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘

#

对象分配与晋升

对象创建 -> Eden区
|
|-> Minor GC后存活 -> Survivor0
|
|-> 再次Minor GC -> Survivor1(复制算法,S0和S1交换)
|
|-> 年龄达到阈值(默认15)-> 老年代
|
|-> 大对象直接进入老年代

#

参数配置

-Xms512m        # 堆初始大小
-Xmx512m # 堆最大大小(建议与初始值相同,避免扩容开销)
-Xmn128m # 年轻代大小
-XX:NewRatio=2 # 老年代:年轻代 = 2:1
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1

5. 方法区(Method Area)

作用:存储类信息、常量、静态变量、即时编译器编译后的代码。

#

JDK 8 之前:永久代(PermGen)

-XX:PermSize=64m      # 永久代初始大小
-XX:MaxPermSize=128m # 永久代最大大小

#

JDK 8 及之后:元空间(Metaspace)

-XX:MetaspaceSize=128m      # 元空间初始大小
-XX:MaxMetaspaceSize=256m # 元空间最大大小(默认无限制)

元空间与永久代的区别

  • 永久代在 JVM 堆中,元空间使用本地内存
  • 元空间大小只受限于物理内存
  • 减少 Full GC 频率

#

方法区存储内容

方法区
├── 类型信息
│ ├── 类名、修饰符
│ ├── 父类、接口
│ └── 方法信息
├── 常量池
│ ├── 字面量(字符串、数字)
│ └── 符号引用
├── 字段信息
├── 方法信息
├── 静态变量
└── JIT编译后的代码

#

运行时常量池

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

6. 直接内存(Direct Memory)

作用:NIO 使用的堆外内存,避免 Java 堆与 Native 堆之间的数据复制。

// 分配直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

// Cleaner 管理内存释放

参数

-XX:MaxDirectMemorySize=128m  # 直接内存上限

注意:直接内存不受 JVM 堆大小限制,但受物理内存限制,也可能导致 OOM。

内存溢出场景

区域 OOM 场景 错误信息
对象过多,无法回收 Java heap space
元空间 动态生成类过多 Metaspace
虚拟机栈 线程过多/递归过深 unable to create new native thread
直接内存 NIO 使用过多 Direct buffer memory

总结

区域 线程 存储内容 异常
程序计数器 私有 字节码行号
虚拟机栈 私有 栈帧、局部变量 StackOverflowError
本地方法栈 私有 Native方法 StackOverflowError
共享 对象实例 OOM
方法区 共享 类信息、常量 OOM
直接内存 共享 NIO缓冲区 OOM

理解 JVM 内存区域,是进行内存调优和 OOM 问题排查的基础。

核心要点

  1. 堆内存分为年轻代和老年代,年轻代又分为 Eden、Survivor 等区域

  2. 栈内存是线程私有的,每个线程都有自己的栈空间

  3. 方法区(元空间)存储类信息、常量池等

  4. 常见的 OOM 类型:HeapSpace、OutOfMemoryError、StackOverflowError

总结

理解 JVM 内存模型是成为高级 Java 工程师的必备技能。在实际项目中,配置合适的堆内存大小、选择合适的垃圾收集器,都需要对内存布局有清晰的认识。


   转载规则


《JVM内存区域划分与作用》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录