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 国际许可协议 进行许可。
  目录