JVM 运行时数据区有哪些?
简化版
JVM 运行时内存分五块:程序计数器、虚拟机栈、本地方法栈(这三块线程私有)和堆、方法区(这两块所有线程共享)。简记:堆放对象、栈放方法调用、方法区放类信息、程序计数器记执行到哪行。堆是最大也是 GC 的主战场。
详细版
线程私有(每个线程一份,随线程生灭):
- 程序计数器(PC):记录当前线程执行到哪条字节码指令。线程切换后能回到正确位置。是唯一不会 OOM 的区域。
- 虚拟机栈(JVM Stack):每调用一个方法就压入一个栈帧(存局部变量表、操作数栈、返回地址等),方法结束栈帧出栈。递归太深会
StackOverflowError。 - 本地方法栈:和虚拟机栈类似,但服务于
native方法(用 C/C++ 写的方法)。
线程共享(整个 JVM 一份):
- 堆(Heap):存放几乎所有对象实例和数组,是内存最大的区域,也是垃圾回收的主战场。分年轻代(Eden + 2 Survivor)和老年代。堆满抛
OutOfMemoryError: Java heap space。 - 方法区(Method Area):规范上保存每个类的结构、运行时常量池、字段和方法数据、方法及构造器代码等。JDK 8+ HotSpot 主要以本地内存中的 Metaspace 承载类元数据,但静态字段引用的对象仍按普通对象关系存活。
完整版教学
一、一张图看清五大区域
┌─────────────── 线程私有(每线程一份)───────────────┐
│ 程序计数器 虚拟机栈(栈帧:局部变量/操作数栈) │
│ 本地方法栈(native 方法) │
└──────────────────────────────────────────────────┘
┌─────────────── 线程共享(全 JVM 一份)──────────────┐
│ 堆 Heap 方法区 Method Area │
│ ├ 年轻代(Eden+S0+S1) (元空间,JDK8 起) │
│ └ 老年代 ├ 类元信息 │
│ ← 对象实例、数组、GC 主战场 ├ 运行时常量池 │
│ └ 静态变量 │
└──────────────────────────────────────────────────┘
判断一块区域是私有还是共享,看它服务谁:服务于方法执行的(PC、两个栈)跟着线程走,是私有的;存对象和类信息的(堆、方法区)大家共用,是共享的。
二、栈和堆的区别——最高频追问
| 维度 | 虚拟机栈 | 堆 |
|---|---|---|
| 存什么 | 栈帧:局部变量、方法调用 | 对象实例、数组 |
| 线程 | 私有 | 共享 |
| 生命周期 | 随方法调用入栈/出栈 | 由 GC 管理 |
| 大小 | 受 -Xss 等影响,默认值依平台/JDK | 受 -Xmx 等影响 |
| 溢出 | StackOverflowError | OutOfMemoryError |
一个经典问题:User u = new User() 这行,对象和引用分别在哪?——new User() 创建的对象在堆里,局部变量 u(指向对象的引用)在虚拟机栈的栈帧里。栈里存的是「地址」,堆里存的是「对象本体」。
三、方法区的演变:永久代 → 元空间
这是 JDK 版本相关的高频点:
- JDK 7 及以前的 HotSpot:主要用 PermGen 实现方法区,容量受
MaxPermSize等限制;它是 JVM 管理的独立区域,不应简单等同于普通 Java 对象堆。 - JDK 8 起的 HotSpot:移除 PermGen,主要改用本地内存中的 Metaspace;未显式设置
MaxMetaspaceSize时仍受进程、容器和操作系统资源限制。
为什么改?永久代大小难调、容易溢出,且和堆的 GC 耦合。挪到本地内存后,元空间大小更灵活,减少了 PermGen 溢出问题。(注意:字符串常量池在 JDK 7 时就已经从永久代挪到了堆里。)
四、哪些区域会 OOM,哪些会 StackOverflow
- 堆 →
OutOfMemoryError: Java heap space(对象太多/泄漏); - 方法区/元空间 →
OutOfMemoryError: Metaspace(类加载器泄漏、动态生成类过多); - 虚拟机栈/本地方法栈 → 栈深超限
StackOverflowError(递归无出口);线程太多分配不出栈 →unable to create new native thread; - 程序计数器 → 唯一不会 OOM 的区域(规范没有为该区域规定 OOM 情形;它保存当前线程执行所需的指令地址信息)。
(这块和「StackOverflowError 和 OutOfMemoryError 的区别」那道题呼应,可对照着记。)
五、规范区域不等于 HotSpot 参数一一映射
JVM 规范描述的是逻辑职责,HotSpot 再选择具体实现。例如“方法区”是规范概念,JDK 8+ HotSpot 主要用本地内存中的 Metaspace 保存类元数据;但运行时常量池、字符串对象和静态字段关联对象的落点不能用一句“全在元空间”概括。
| 需求/数据 | 常见 HotSpot 位置 | 主要风险或参数 |
|---|---|---|
| Java 对象和数组 | Java 堆 | -Xms、-Xmx,heap OOM |
| Java 栈帧 | 每线程虚拟机栈 | -Xss,SOE 或线程创建压力 |
| 类元数据 | 本地 Metaspace | MaxMetaspaceSize,Metaspace OOM |
| JIT 机器码 | Code Cache | 代码缓存满导致编译受限 |
| NIO 直接缓冲区 | 堆外本地内存 | MaxDirectMemorySize 等 |
“元空间默认无限”也不准确:未显式设上限时,它会按需向本地内存扩展,但仍受进程地址空间、容器限制和操作系统可用内存约束。
六、用总内存预算理解线上 OOM
假设容器限制 4 GB,配置堆 3 GB、200 个线程且每线程栈上限 1 MB,单线程栈理论预算已约 200 MB;再加元空间 200 MB、直接内存 400 MB、代码缓存和 GC 结构,进程很容易逼近容器上限。
进程总内存 ≈ Java 堆 + 线程栈 + 元空间 + 代码缓存
+ 直接/本地内存 + GC/JVM 结构 + 本地库
各项不会始终同时用满,但容量规划必须留安全余量。堆 dump 只能解释 Java 堆,若 RSS 持续上涨而堆稳定,应进一步看 Native Memory Tracking、线程数、直接缓冲区和本地库。
程序计数器是线程私有的逻辑区域,规范中它是少数没有规定 OutOfMemoryError 情形的区域;虚拟机栈和本地方法栈的具体实现可以合并,不能把教材示意图机械等同于物理分配块。
看到 OOM 消息时先定位内存区域;“加大 Xmx”只对堆容量问题可能有效,对元空间、直接内存或线程创建失败未必有帮助。
七、常见误区与追问
- 误区:运行时数据区就是操作系统物理内存的固定切片。 它首先是 JVM 规范的逻辑模型,具体映射由实现决定。
- 误区:JDK 8 后所有方法区数据都搬进了元空间。 类元数据主要在 Metaspace,但字符串对象等仍有各自运行时位置。
- 误区:元空间不设上限就永远不会 OOM。 它仍受本地内存、地址空间和容器限制约束。
- 追问:堆稳定但进程 RSS 上涨查什么? 查线程栈、DirectBuffer、元空间、代码缓存、本地库与 NMT 分类。
- 追问:栈里的局部变量对象也在栈吗? 局部变量槽通常保存引用,常规未消除的对象存储仍在堆;JIT 标量替换另当别论。
- 追问:方法区和永久代是什么关系? 方法区是规范概念,永久代是旧 HotSpot 的一种实现,JDK 8 起由 Metaspace 等机制取代。
- 追问:为什么线程太多会导致 OOM? 每个线程需要栈和本地资源,总预算不足时会无法创建新线程。
八、加强记忆
用“线程执行跟线程走,对象与类信息供线程共享”区分程序计数器、两类栈与堆、方法区,但这些首先是 JVM 规范的逻辑区域。JDK 8+ HotSpot 用 Metaspace 承载主要类元数据,它来自本地内存且仍受进程、容器和系统资源限制。进程总内存还包括线程栈、代码缓存、直接内存、GC 结构和本地库,所以 Xmx 不能贴满容器上限。遇到 OOM 先看错误消息定位 heap、Metaspace、direct 或 native thread,再选择 heap dump、NMT、线程统计等对应证据。