Skip to content

03 - 运行时数据区

定位: JVM 运行时内存的全景划分——程序计数器到直接内存,7 大区域的设计原理与协作关系 面试高频度: ⭐⭐⭐⭐⭐

一、运行时数据区在 JVM 体系中的位置

类加载将 Class 文件的静态结构转化为方法区的运行时元数据后,JVM 就需要一片运行时内存来执行实际的计算。运行时数据区就是这片内存的划分方案:

  • 向上: 接收类加载器的输出(类元数据存入方法区,Class 对象存入堆)
  • 向下: 为执行引擎提供运行时内存(指令在栈帧上执行,对象在堆上分配)
  • 横向: 与 GC 系统紧密耦合——GC 控制堆区域的生死循环

二、JVM 运行时内存全景布局

┌─────────────────────────────────────────────────────────────────────────────┐
│                         JVM 运行时内存全景                                      │
│                                                                             │
│  ┌─────────── 线程私有 (N 个线程, N 份副本) ──────────────────┐               │
│  │                                                           │               │
│  │  Thread 1                     Thread 2                    │               │
│  │  ┌───────────────────┐       ┌───────────────────┐       │               │
│  │  │ 程序计数器 (PC)    │       │ 程序计数器 (PC)    │       │               │
│  │  │   → 记录当前字节码  │       │   → 记录当前字节码  │       │               │
│  │  │     行号指示器     │       │     行号指示器     │       │               │
│  │  └───────────────────┘       └───────────────────┘       │               │
│  │  ┌───────────────────┐       ┌───────────────────┐       │               │
│  │  │ Java虚拟机栈       │       │ Java虚拟机栈       │       │               │
│  │  │  ┌─ 栈帧 1 ──────┐│       │  ┌─ 栈帧 1 ──────┐│       │               │
│  │  │  │ 局部变量表     ││       │  │ 局部变量表     ││       │               │
│  │  │  │ 操作数栈       ││       │  │ 操作数栈       ││       │               │
│  │  │  │ 动态链接       ││       │  │ 动态链接       ││       │               │
│  │  │  │ 返回地址       ││       │  │ 返回地址       ││       │               │
│  │  │  └───────────────┘│       │  └───────────────┘│       │               │
│  │  │  ┌─ 栈帧 2 ──────┐│       │  ┌─ 栈帧 2 ──────┐│       │               │
│  │  │  │ ...            ││       │  │ ...            ││       │               │
│  │  │  └───────────────┘│       │  └───────────────┘│       │               │
│  │  └───────────────────┘       └───────────────────┘       │               │
│  │  ┌───────────────────┐       ┌───────────────────┐       │               │
│  │  │ 本地方法栈         │       │ 本地方法栈         │       │               │
│  │  │  (HotSpot: 与      │       │  (HotSpot: 与      │       │               │
│  │  │   VM栈合并)        │       │   VM栈合并)        │       │               │
│  │  └───────────────────┘       └───────────────────┘       │               │
│  └───────────────────────────────────────────────────────────┘               │
│                                   │                                           │
│                                   ▼                                           │
│  ┌─────────── 线程共享 ─────────────────────────────────────┐                 │
│  │                                                          │                 │
│  │  ┌────────────────────────────────────────────────────┐  │                 │
│  │  │  堆 (Heap)                                           │  │                 │
│  │  │  ┌───────────┬──────────┬──────────┬──────────────┐ │  │                 │
│  │  │  │ Eden      │ S0       │ S1       │ Old /        │ │  │                 │
│  │  │  │ (TLAB区域) │ (存活期1) │ (存活期2) │ 老年代       │ │  │                 │
│  │  │  │           │          │          │              │ │  │                 │
│  │  │  │ Thread1   │ ←Eden的  │ ←S0或S1  │ ←长期存活的对象│ │  │                 │
│  │  │  │ Thread2   │   幸存者  │   晋升来  │              │ │  │                 │
│  │  │  │ Thread3   │          │          │              │ │  │                 │
│  │  │  └───────────┴──────────┴──────────┴──────────────┘ │  │                 │
│  │  │  字符串常量池 (JDK 7+) ← 堆的一部分                   │  │                 │
│  │  └────────────────────────────────────────────────────┘  │                 │
│  │                                                          │                 │
│  │  ┌────────────────────────────────────────────────────┐  │                 │
│  │  │  方法区 / 元空间 (Method Area / Metaspace)          │  │                 │
│  │  │  ├ 类元数据 (Class Metadata)                       │  │                 │
│  │  │  ├ 运行时常量池 (Runtime Constant Pool)             │  │                 │
│  │  │  ├ 方法字节码 (Code 属性)                          │  │                 │
│  │  │  ├ JIT 编译代码缓存                                │  │                 │
│  │  │  └ 字段/方法/接口信息                              │  │                 │
│  │  │  (JDK 8+: 使用本地内存, 不受堆大小限制)              │  │                 │
│  │  └────────────────────────────────────────────────────┘  │                 │
│  │                                                          │                 │
│  │  ┌────────────────────────────────────────────────────┐  │                 │
│  │  │  直接内存 (Direct Memory / Off-Heap)                │  │                 │
│  │  │  ├ NIO DirectByteBuffer (通过 Unsafe 分配)          │  │                 │
│  │  │  ├ 本地代码分配的内存 (JNI 调用)                     │  │                 │
│  │  │  └ 不受 -Xmx 限制, 受 OS 可用内存限制               │  │                 │
│  │  └────────────────────────────────────────────────────┘  │                 │
│  └──────────────────────────────────────────────────────────┘                 │
└─────────────────────────────────────────────────────────────────────────────┘

为什么这样划分?

线程私有 vs 线程共享 的划分,本质上是生命周期管理和 GC 效率的产物:

分区包含区域生命周期GC 是否介入
线程私有程序计数器、虚拟机栈、本地方法栈随线程生灭,无需同步不参与 GC
线程共享堆、方法区/元空间随 JVM 生灭,需同步控制堆是 GC 主战场

线程私有数据天生是线程安全的——一个线程无法访问另一个线程的栈帧或 PC。这消除了同步开销,也让栈区域成为了 GC 的"盲区":因为栈帧弹出时内存自然释放,不需要 GC 来回收。

三、子专题导航

#主题面试频率核心内容
**程序计数器**程序计数器⭐⭐字节码行号指示器、线程切换恢复、唯一没有 OOM 的区域
**Java虚拟机栈**Java 虚拟机栈⭐⭐⭐⭐⭐栈帧 4 组件、StackOverflowError vs OOM、局部变量表 slot 复用
**本地方法栈**本地方法栈⭐⭐JNI 调用栈、HotSpot 栈合并、native 方法执行
**Java堆**Java 堆⭐⭐⭐⭐⭐分代设计、TLAB、Eden:S0:S1 比例、对象分配与晋升
**方法区与元空间**方法区与元空间⭐⭐⭐⭐PermGen→Metaspace 迁移、类元数据、JIT 缓存
**运行时常量池**运行时常量池⭐⭐⭐两级常量池设计、符号引用→直接引用、String.intern()
**直接内存**直接内存⭐⭐⭐零拷贝 I/O 原理、DirectByteBuffer 回收、Off-Heap OOM

四、核心考点速记

1. 栈 vs 堆 —— 核心区别

维度栈 (Stack)堆 (Heap)
存储内容局部变量、方法调用上下文对象实例、数组
生命周期随方法生命周期:方法入栈→帧创建,方法返回→帧销毁随对象生命周期:GC 判定不可达后回收
访问速度快(紧贴 CPU,通过栈指针访问)慢(GC 要追踪引用链)
线程安全天生安全(线程私有)需同步(volatile、synchronized)
异常情况StackOverflowError(递归太深),OOM(线程太多)OOM(对象无法被 GC)
内存分配方式编译期确定 slot 大小,运行期固定运行期动态分配,GC 自动回收

2. 方法区变迁 (PermGen → Metaspace)

JDK 6 及以前                          JDK 8+
┌─────────────────────────────┐      ┌─────────────────────────────┐
│       堆                     │      │       堆                     │
│  ┌─────────┬──────────────┐ │      │  ┌─────────┬──────────────┐ │
│  │ Young   │ Old          │ │      │  │ Young   │ Old          │ │
│  ├─────────┴──────────────┤ │      │  ├─────────┴──────────────┤ │
│  │ 字符串常量池            │ │      │  │ 字符串常量池            │ │
│  └────────────────────────┘ │      │  └────────────────────────┘ │
├─────────────────────────────┤      ├─────────────────────────────┤
│  PermGen (永久代)             │      │  Metaspace (本地内存)        │
│  ├ 类元数据                  │      │  ├ 类元数据                  │
│  ├ 运行时常量池              │      │  ├ 运行时常量池              │
│  └ 字符串常量池 (JDK6及以前)  │      │  └ JIT 编译代码缓存           │
│                              │      │                             │
│  固定大小(-XX:MaxPermSize)    │      │  默认无上限(OS 内存决定)     │
│  字符串常量池在 PermGen →     │      │  -XX:MaxMetaspaceSize 可限制 │
│  String.intern() 易 OOM      │      │  类卸载条件更宽松            │
└─────────────────────────────┘      └─────────────────────────────┘

为什么迁移? PermGen 固定大小且难以调优,生产环境中 String.intern() 和类加载容易导致 OOM。Metaspace 使用本地内存,由 OS 管理,不再有固定的上限,解决了"调大浪费、调小 OOM"的两难问题。

3. 直接内存 (Direct Memory)

传统 I/O 路径 (堆内存):
磁盘 → [内核缓冲区] → [JVM 堆缓冲区] → 应用代码
       ①系统拷贝     ②系统拷贝        ③无拷贝
                        ↑ CPU 参与两次拷贝

NIO I/O 路径 (直接内存):
磁盘 → [内核缓冲区] → [直接内存] → 应用代码
       ①系统拷贝     ②(共享映射)
                        ↑ DMA 直接写入, CPU 拷贝减少一次

直接内存绕过堆的限制,使用操作系统的 DMA 能力实现更高效的 I/O。但它的回收依赖于 Cleaner 机制——DirectByteBuffer 被 GC 时才触发 Unsafe.freeMemory()。如果堆一直不 GC,直接内存可能被耗尽,造成一种特殊的 OOM:堆明明还有空间,但系统内存不足了。

五、面试高频追问一览

追问关联笔记频次
JVM 的运行时数据区分为哪几块?哪些是线程私有的?**运行时数据区索引**极高
栈和堆有什么区别?**运行时数据区索引**极高
方法区从 PermGen 变成 Metaspace 的原因?**方法区与元空间**极高
程序计数器会不会 OOM?**程序计数器**
Java 虚拟机栈会抛出哪些异常?**Java虚拟机栈**
堆为什么要分代?**Java堆**极高
什么是 TLAB?**Java堆**
String.intern() 的底层原理?**运行时常量池**
直接内存和堆内存有什么区别?**直接内存**中高
符号引用和直接引用的区别?**运行时常量池**

📚 相关文件

  • **Class文件结构** — 常量池的概念入口(运行时常量池的静态来源)
  • **类加载生命周期** — 类加载在方法区中构建运行时数据结构
  • **对象创建过程** — 对象在堆上分配的具体流程
  • **对象存活判定算法** — GC 在堆上判定对象是否存活的算法
  • **JVM文库设计文档**

Knowledge4J — Java 知识库