JVM 内存区域与内存溢出
2026/8/24大约 4 分钟
JVM 内存区域与内存溢出
JVM 运行时数据区详解:程序计数器、虚拟机栈、本地方法栈、Java 堆、方法区、直接内存。
运行时数据区总览
JVM 所管理的内存分为以下几个运行时数据区:
| 区域 | 线程共享 | 存储内容 | 异常 |
|---|---|---|---|
| 程序计数器 | 私有 | 当前线程执行的字节码行号 | 无 OOM |
| Java 虚拟机栈 | 私有 | 栈帧(局部变量表、操作数栈等) | StackOverflowError / OOM |
| 本地方法栈 | 私有 | Native 方法调用 | StackOverflowError / OOM |
| Java 堆 | 共享 | 几乎所有对象实例和数组 | OOM |
| 方法区 | 共享 | 类信息、常量、静态变量、编译后代码 | OOM |
各区域详解
程序计数器(PC Register)
- 当前线程所执行字节码的行号指示器,字节码解释器通过改变计数值选择下一条指令
- 分支、跳转、循环都依赖它
- 线程私有,各线程互不影响
- 执行 Java 方法时记录字节码指令地址;执行 Native 方法时值为空
- 唯一一个没有规定任何 OOM 情况的区域
Java 虚拟机栈
- 线程私有,生命周期与线程相同
- 描述 Java 方法执行的内存模型:每个方法执行时创建一个栈帧
- 只有栈顶栈帧有效(当前栈帧),对应方法为当前方法
- 栈帧存储:局部变量表、操作数栈、动态链接、方法返回地址及附加信息
局部变量表:存放方法参数和局部变量,容量以变量槽(Slot)为最小单位;32 位数据类型占 1 个 Slot,64 位(long/double)占 2 个连续 Slot;实例方法第 0 位 Slot 默认传递 this 引用;Slot 可重用。
操作数栈:最大深度编译期确定;Java 虚拟机是"基于栈的执行引擎"(Android 虚拟机基于寄存器)。基于栈优点是可移植性强,缺点是执行速度相对慢;基于寄存器优点执行快,缺点可移植性差。
动态链接:每个栈帧指向运行时常量池中该栈帧所属方法的引用;符号引用一部分在类加载时静态解析为直接引用,一部分运行期动态连接。
方法返回地址:正常退出时返回地址为调用者 PC 计数器值;异常退出时通过异常处理器确定。
本地方法栈
与虚拟机栈作用相似,但为本地操作系统(Native)方法服务。
Java 堆(GC 堆)
- JVM 管理的最大一块内存,所有线程共享
- 几乎所有对象实例和数组都在此分配
- 垃圾收集器管理的主要区域
- 物理上可不连续,逻辑上连续即可;堆无法扩展时抛 OutOfMemoryError
方法区(永久代)
- 各线程共享,存储:已加载的类信息、常量、静态变量、即时编译器编译后的代码
- HotSpot 中称"永久代"(JRockit、IBM J9 无此概念)
- 垃圾收集行为较少,主要回收废弃常量和无用类
- 运行时常量池是方法区的一部分,具备动态性(如
String.intern()运行期放入新常量)
直接内存
- 不是 JVM 运行时数据区,直接从操作系统分配内存,不受 Java 堆大小限制
- 受本机总内存及处理器寻址空间限制,也可能 OOM
- Java 1.4 引入 NIO,在堆外分配内存,避免 Java 堆和 Native 堆之间来回复制数据,某些场景提高性能
内存泄漏与内存溢出的区别
| 概念 | 定义 |
|---|---|
| 内存泄漏 | 分配出去的内存没被回收,失去控制造成资源浪费。Java 中一般不会产生(有 GC),但持有无用对象的引用时仍会泄漏 |
| 内存溢出 | 程序所需内存超出系统能分配的上限(包括动态扩展) |
各区域内存溢出测试要点
- 堆 OOM:不断创建大对象/集合且保持引用
- 栈溢出:无限递归 → StackOverflowError(单线程下栈空间无法分配抛的是 StackOverflowError,多线程下抛 OOM)
- 方法区 OOM:大量动态生成类(如 CGLIB 代理)