JVM虚拟机:运行时数据区域总览

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 限制,易被忽略
  1. 当你 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 的内存溢出错误。

相关推荐
江君是实在人2 小时前
java jvm 调优
java·开发语言·jvm
阿崽meitoufa2 小时前
JVM虚拟机:垃圾收集算法
java·jvm·算法
程序员敲代码吗4 小时前
使用Python进行PDF文件的处理与操作
jvm·数据库·python
佛祖让我来巡山4 小时前
【面试题】为什么 Java 8 移除了永久代(PermGen)并引入了元空间(Metaspace)?
jvm·元空间·永久代
风送雨5 小时前
FastAPI 学习教程 · 第5部分
jvm·学习·fastapi
程序员敲代码吗6 小时前
如何从Python初学者进阶为专家?
jvm·数据库·python
萧曵 丶7 小时前
JVM 字节码指令浅谈
jvm·类加载
期待のcode7 小时前
浅堆深堆与支配树
java·jvm·算法
不穿格子的程序员8 小时前
JVM篇2:根可达性算法-垃圾回收算法和三色标记算法-CMS和G1
java·jvm·g1·根可达性算法·三色标记算法