
Java 虚拟机在运行 Java 程序时,会将管理的内存划分为若干个区域。这些区域根据生命周期可以分为 "线程私有" 和 "线程共享" 两大类。
1. 线程私有区域 (Thread Private)
这些区域的生命周期与线程相同,随线程启动而创建,随线程结束而销毁。
2.2.1 程序计数器 (Program Counter Register)
-
作用: 当前线程所执行的字节码的行号指示器。
-
如果执行的是 Java 方法,记录的是正在执行的指令地址。
-
如果执行的是 Native 方法,数值为空 (Undefined)。
-
-
特点:
-
内存空间较小。
-
唯一 一个在 JVM 规范中没有规定
OutOfMemoryError的区域。 -
它是程序控制流(分支、循环、异常处理等)的基础。
-
2.2.2 Java 虚拟机栈 (Java Virtual Machine Stack)
-
作用: 描述 Java 方法执行的线程内存模型。
-
核心组件: 每个方法执行时都会创建一个 栈帧 (Stack Frame)。
-
局部变量表: 存放基本数据类型、对象引用、returnAddress。大小在编译期确定(以变量槽 Slot 为单位)。
-
操作数栈
-
动态连接
-
方法出口
-
-
异常情况:
-
StackOverflowError:线程请求的栈深度大于允许深度。 -
OutOfMemoryError:如果栈允许动态扩展,但扩展时申请不到足够内存(HotSpot 虚拟机栈通常不可动态扩展,但在申请内存失败时仍会 OOM)。
-
2.2.3 本地方法栈 (Native Method Stacks)
-
作用: 与虚拟机栈类似,但它是为虚拟机使用到的 本地 (Native) 方法 服务。
-
特点: 具体的虚拟机可以自由实现(如 HotSpot 将其与虚拟机栈合二为一)。
2. 线程共享区域 (Thread Shared)
这些区域随虚拟机启动而创建,所有线程共享。
2.2.4 Java 堆 (Java Heap)
-
地位: JVM 管理的内存中最大的一块。
-
作用: 存放 对象实例 和 数组("几乎"所有对象都在此分配)。
-
垃圾收集 (GC): 是垃圾收集器管理的重点区域,因此常被称为 "GC 堆"。
-
经典分代(历史观点): 新生代 (Eden, Survivor)、老年代、永久代(JDK 8 已废弃)。
-
内存分配优化: 可能划分出线程私有的分配缓冲区 (TLAB) 以提升效率。
-
-
异常: 无法完成实例分配且堆无法扩展时,抛出
OutOfMemoryError。
2.2.5 方法区 (Method Area)
-
别名: 非堆 (Non-Heap)。
-
作用: 存储已被加载的 类型信息、常量、静态变量、即时编译后的代码缓存。
-
演变历史:
-
JDK 7 及以前: HotSpot 使用 "永久代" (Permanent Generation) 来实现方法区,容易导致内存溢出。
-
JDK 7: 字符串常量池、静态变量移出永久代。
-
JDK 8 及以后: 彻底废弃永久代,改用本地内存实现的 元空间 (Metaspace)。
-
-
2.2.6 运行时常量池 (Runtime Constant Pool):
-
是方法区的一部分。
-
存放编译期生成的字面量和符号引用,以及运行期间产生的常量(如
String.intern())。
-
3. 非运行时数据区 (系统内存)
2.2.7 直接内存 (Direct Memory)
-
定义: 不是 JVM 规范定义的运行时数据区,属于堆外内存。
-
应用场景: JDK 1.4 引入的 NIO 类,使用 Native 函数库直接分配堆外内存,通过 Java 堆中的
DirectByteBuffer对象引用。 -
优势: 避免了在 Java 堆和 Native 堆之间来回复制数据,提升性能。
-
风险: 不受 JVM 堆大小限制,但受本机物理内存限制。配置
-Xmx时若忽略此部分,可能导致总内存超标而引发 OOM。
总结对比表
| 区域名称 | 是否线程私有 | 核心存储内容 | 潜在异常 | 关键备注 |
|---|---|---|---|---|
| 程序计数器 | 是 | 下一条指令地址 | 无 | 唯一无 OOM 的区域 |
| 虚拟机栈 | 是 | 栈帧 (局部变量、操作数栈) | SOE, OOM | 方法调用模型,注意递归深度 |
| 本地方法栈 | 是 | Native 方法相关 | SOE, OOM | HotSpot 将其与虚拟机栈合一 |
| Java 堆 | 否 (共享) | 对象实例、数组 | OOM | GC 的主战场,最大区域 |
| 方法区 | 否 (共享) | 类信息、静态变量、常量 | OOM | JDK 8 后变为 Metaspace (本地内存) |
| 直接内存 | 否 (共享) | 堆外数据 (NIO) | OOM | 不受 -Xmx 限制,易被忽略 |
- 当你 new 一个对象时,这个对象放在哪?指向它的引用放在哪?(堆 和 栈)
对象实例 存放在Java 堆 (Java Heap) 中,因为 Java 堆是 JVM 管理的最大一块内存区域,其唯一目的就是存放对象实例。 指向它的引用 ,如果是在方法中定义的局部变量,则存放在Java 虚拟机栈 (JVM Stack) 中当前方法的栈帧内的局部变量表中。
2.当你写一个递归方法没写终止条件时,哪个区域会爆?(虚拟机栈)
Java 虚拟机栈 (JVM Stack) 会爆。 每次方法调用都会创建一个新的栈帧并入栈。无限递归意味着方法不断调用自身且不返回,导致栈帧不断积压,最终线程请求的栈深度会超过虚拟机允许的最大深度,从而抛出 StackOverflowError 异常。
3.当你定义一个 static final String str = "hello"; 时,它存在哪?(方法区/常量池)
它总体上存在于方法区 (Method Area)。 具体来说:
-
static修饰说明它是静态变量,静态变量存储在方法区中。 -
字符串字面量
"hello"属于编译期生成的字面量,会被存放到方法区内部的运行时常量池 (Runtime Constant Pool) 中。
4.为什么 JDK 8 之后很少见到 java.lang.OutOfMemoryError: PermGen space 了?(因为 PermGen 没了,变成了 Metaspace)
因为从 JDK 8 开始,HotSpot 虚拟机完全废弃了"永久代" (PermGen) 的概念 。 在此之前,永久代用于实现方法区,且有固定的大小上限,容易触发 OOM。JDK 8 之后,改用在本地内存 (Native Memory) 中实现的元空间 (Metaspace) 来代替永久代存储类型信息等数据。由于元空间使用的是本地内存,其大小只受本机总可用内存的限制(除非显式设置了 MaxMetaspaceSize),因此不再会出现 PermGen space 的内存溢出错误。