{"name":"内存结构","id":"编程语言-JAVA-JVM-自动内存管理-内存结构","content":"# JVM 内存结构\n\n> JVM 内存结构的本质，不在\"有哪些区域\"，而在\"为何这样划分\"——\n> 它是 **字节码执行模型与多线程并发模型** 的工程投影。\n\n## 设计起点：从需求到结构\n\n理解 JVM 内存结构，不应从\"有哪些区域\"入手，而应回答一个前置问题：\n\n**为什么 JVM 需要一套内存结构？它由什么力量塑形？**\n\n三股独立驱动力，收敛出同一套结构。\n\n### 驱动力一·执行模型：决定\"要存什么\"\n\nJVM 的运行本质是执行字节码——方法调用与返回、操作数计算、对象创建。这套执行机制反向要求四类运行时状态必须有处可存：\n\n| 执行机制 | 要物化的状态 |\n| --- | --- |\n| 指令顺序推进、可跳转 | 执行位置 |\n| 方法调用与返回 | 调用上下文 |\n| 操作数计算 | 计算暂存 |\n| 对象创建与类型引用 | 对象数据 + 类型元数据 |\n\n执行模型只回答\"**要存什么**\"，不回答\"**怎么摆**\"。后者由另两股力量决定。\n\n### 驱动力二·并发 + 驱动力三·GC：从两个方向收敛到同一刀\n\nJVM 天然是多线程执行环境，同时又要托管自动内存管理。二者出发点不同，却指向**同一条分界线**：\n\n- **并发要求隔离与共享并存**：每个线程有独立执行流，执行状态必须**私有**（切换时互不污染）；但线程要协作处理同一批数据，对象必须**共享**。\n- **GC 要求边界清晰**：自动回收的前提是分得清\"哪些是对象数据（要管理生命周期）、哪些是执行状态（随栈帧自然消亡）\"。\n\n两股力量，一把刀：\n\n> **执行上下文 → 私有；对象数据 → 共享。**\n> 并发从\"安全\"出发、GC 从\"可回收\"出发，独立推理却落在同一处切分——这是内存结构最稳定的骨架。\n\n### 两类数据的分离\n\n于是所有运行时数据归为两类，自然形成两大区域：\n\n| 数据类型 | 本质 | 归属 |\n| --- | --- | --- |\n| 执行上下文 | 描述\"代码执行到哪里\" | 线程私有 |\n| 对象数据 | 描述\"程序处理的数据\" | 线程共享 |\n\n### 分区框架\n\n```\nJVM 内存结构（HotSpot）\n├── 线程私有区（执行上下文）\n│   ├── 程序计数器（PC）\n│   ├── 虚拟机栈（JVM Stack）\n│   └── 本地方法栈（Native Stack）\n│\n└── 线程共享区（数据存储）\n    ├── 堆（Heap）\n    ├── 方法区（Method Area）\n    ├── 运行时常量池\n    └── 直接内存\n```\n\n## 规范与实现\n\n《Java 虚拟机规范》锁定的只是**行为契约**——\"有一个存放类元数据的逻辑职责\"\"方法调用需要栈帧\"。而这些职责**落在内存的哪里、是否连续、是否被 GC、是否与相邻区合并**，规范一概不管：\n\n> 运行时数据区的**内存布局**、所用的**垃圾回收算法**、以及虚拟机指令的**内部优化**（如翻译为机器码），全部\"留给实现者自行裁量\"。\n\n> 规范唯一、实现多样，实现间差异是工程权衡而非理论分歧（见 [JVM](/编程语言/JAVA/JVM/JVM.md) \"JVM 实现的多样性\"一节）。内存结构是这一判断在\"数据区\"层的投影。\n\n行为锁定、布局放开，直接把所有运行时概念劈成两层——一层随规范恒定，一层随实现浮动：\n\n| 层次 | 属性 | 成员 | 是否随实现 / 版本变 |\n| ---- | ---- | ---- | ---------------- |\n| **逻辑概念** | 公共、抽象、规范锁定 | 方法区、运行时常量池、字符串常量池、栈帧职责 | 否——稳定原理层 |\n| **物理概念** | 私有、具体、实现裁量 | Heap、Metaspace、PermGen、直接内存的物理落地 | 是——可替换实现层 |\n\n> 类比：**方法区之于 Metaspace，如同接口之于实现类。**\n> 规范定义\"方法区\"这个接口（存类元数据的职责），PermGen（JDK7 及以前）与 Metaspace（JDK8+）是两个实现类。\n\n### 分歧的模式：规范留白处，即实现分歧处\n\n规范每一处\"未规定 / 允许 / 不属于\"，都是留给实现的填空题；填法由工程权衡（省内存、简化、性能）决定，于是同一逻辑区在不同实现与版本间落点各异。\n\n- **位置留白**（方法区、字符串常量池）→ 实现在堆内 / 堆外间选，且随版本迁移\n- **合并留白**（本地方法栈）→ HotSpot 择与虚拟机栈合一，规范亦允许独立 C 栈\n- **归属留白**（直接内存）→ 规范外，实现自行在 native 分配\n\n> 方法区，规范原文称它\"逻辑上属于堆\"（*logically part of the heap*），HotSpot 却在 JDK8 后把它搬到了堆外、乃至 JVM 托管内存之外的 Metaspace。规范说\"属于堆\"、实现搬出堆——**位置、归属、边界全可被实现推翻；不可推翻的只有\"逻辑职责\"这一契约。**\n\n## 线程私有区：执行上下文模型\n\n线程私有区的本质是：描述“一个线程的执行现场——它进行到哪、调用链如何、当前在算什么”\n\n### 程序计数器\n\n程序计数器的本质是**线程级的执行状态指针**——记录当前线程执行到哪条字节码指令。\n\n**为何需要它**：JVM 多线程靠时间片轮转、抢占式调度切换执行。线程被切走再切回时，得知道\"上次执行到哪\"才能续上，PC 就是这个断点，它保证了单线程执行的连续性。\n\n**唯一不会 OOM 的区域**：PC 只存一个指令地址，容量恒定、不随程序增长——是 JVM 中唯一没有 OOM 可能的内存区。\n\n### 方法调用栈：虚拟机栈与本地方法栈\n\n两者是**同一模型的两个实例**：线程私有、后进先出，**一次方法激活对应一个栈帧**，入栈即调用、出栈即返回——栈结构本身就是\"方法调用链\"的运行时建模。\n\n**为何是栈**：方法调用天然嵌套，后调用的先返回，调用间需保存、返回时需恢复现场——这正是 LIFO 语义。\n\n**栈帧结构**（虚拟机栈，规范定义）：\n\n```\n栈帧\n├── 局部变量表  —— 方法的私有数据空间\n├── 操作数栈    —— 字节码指令的计算暂存区\n├── 动态连接    —— 与运行时常量池的绑定\n└── 返回地址    —— 调用链的恢复点\n```\n\n**唯一的分叉——服务哪个世界**：\n\n| | 虚拟机栈 | 本地方法栈 |\n| --- | --- | --- |\n| 服务对象 | Java（字节码）方法 | Native 方法 |\n| 栈帧内部 | 上表四件套，规范锁定 | 由 native 实现自定，规范不管 |\n\n本地方法栈是同一栈模型在 **Java / Native 执行边界**另一侧的落地。HotSpot 直接把二者**合二为一**。\n\n**异常语义**（两栈共有）：容量有限——调用过深抛 `StackOverflowError`，栈扩展时申请不到内存抛 `OutOfMemoryError`。\n\n## 线程共享区：数据存储模型\n\n线程共享区的本质是：存储**程序的状态数据**——被多线程处理、需跨方法调用存活的对象；区别于私有区那些随方法执行产生、又随退栈消亡的**过程数据**。\n\n### 堆\n\n堆的本质是**对象生命周期管理区**——存储所有对象实例与数组，并承载 GC。\n\n**为何需要独立的堆**：三个条件同时成立，栈便无法承载，堆成为默认解——\n\n| 条件 | 栈为何不行 | 若该条件不成立，可换的解 |\n| --- | --- | --- |\n| 生命周期编译期不定 | 栈帧随方法退出即销毁，装不下活得更久的对象 | 编译期能定生死 → 所有权 + 确定性析构，无需堆 |\n| 多线程需共享对象 | 栈是线程私有，别的线程看不见 | 不共享内存、只传消息 → 每线程/进程私有内存 |\n| 需统一自动回收 | 栈无法处理乱序存活的对象 | 手动释放 / 整块释放 → Arena 区域分配 |\n\n堆是 **\"生命周期不定 + 需共享 + 要自动回收\"三者叠加时的一个工程默认解**。\n\n堆的分代：\n\n> **按对象的某种统计规律分区，对不同区施加匹配该规律的策略——用分类换取整体效率。**\n\n| 分区维度 | 派生模型 | 依赖的假说 |\n| --- | --- | --- |\n| 年龄 | **分代** | 多数对象朝生夕死 |\n| 空间（等大 Region） | **分区**（G1） | 可按回收收益挑区，而非整代 |\n| 大小 | **大对象区** | 大对象少、移动贵、常偏长寿 |\n| 引用拓扑 / 可变性 / NUMA 位置 | 连通性 GC 等 | 各自的局部性 / 存活规律 |\n\n**TLAB**：堆是共享区，多线程同时分配会争抢同一个分配指针。TLAB 让每个线程持有一段私有分配区，把分配热路径**从共享退化为私有**、由争锁变为无锁——**用空间换并发**。它是\"共享区在热点上局部私有化\"的典型手法，也是前文\"私有 / 共享\"分界在堆内部的一次回响。\n\n### 方法区\n\n方法区的本质是 JVM 的**类型信息仓库**——存储类结构信息、常量、静态变量、以及 JIT 编译后的代码。\n\n**演进：PermGen → Metaspace**（JDK8 分界）。根因是**将类元数据与 Java 堆解耦**：脱离 `-Xmx` 与堆 GC 后，Metaspace 默认无上限、按需扩展，解决了 PermGen \"大小难调\"之痛——**但溢出并未消除**，只是从\"堆内固定小区易撑爆\"变为\"无节制增长可能触发进程级 OOM\"。\n\n### 运行时常量池\n\n运行时常量池是**常量的运行时表示**，属方法区的逻辑组成。\n\n**动态性**是其关键：它不止装编译期常量，`String.intern()` 可在运行期向池中注入新常量——体现常量池的运行时扩展能力，而非一份静态只读表。\n\n### 直接内存\n\n直接内存的本质是**进程地址空间里的 Native Heap**——经 NIO / Unsafe 在 JVM 堆外分配，不受 `-Xmx` 管辖，但会触发 OOM。\n\n**动机**是消除拷贝：NIO 的 Zero Copy 让 I/O 直接读写这块 native 内存，省去\"Java 堆 ↔ Native 堆\"的来回复制。**代价**是脱离 GC 托管，需手动或借 `Cleaner` 释放。\n\n## 对象模型：从创建到存储\n\n前面讲的\"区\"是容器，对象是容器里真正被操作的单元。它的三个面——**怎么生、怎么排、怎么被找到**——各沉淀一条可迁移的权衡。贯穿它们的是一个原则：**对象是自描述单元**，自带类型与运行时状态元数据，故执行引擎（多态）、GC（追踪）、并发（加锁）三方能协同操作它，无需外部登记表。\n\n### 创建：分配与初始化的两阶段分离\n\n`Object obj = new Object()` 编译为 `new → dup → invokespecial → astore`，其稳定内核不在这几条指令，而在它们揭示的**两阶段**：\n\n```\n分配内存 + 零值填充  →  执行 <init> 构造\n  （对象已存在）        （对象才完备）\n```\n\n- **为何分离**：内存须先到位且带确定零值，构造逻辑才有处可写。这决定了存在一个\"已分配、未构造完\"的中间窗口。\n- **可迁移含义**：该窗口是并发可见性、`this` 逃逸、`final` 语义问题的共同根源——半构造对象一旦被别的线程看到，读到的是零值而非构造结果。\n\n> 指令只是该协议的投影：`new` 只做\"分配 + 压引用\"，`dup` 留一份给构造消耗、一份给赋值——**字节码层面就把\"分配\"与\"初始化\"劈开了**。\n\n### 布局：自描述的代价与对齐\n\n```\n对象\n├── 对象头\n│   ├── 类型指针    —— 指向类元数据：对象\"是什么\"，多态分派与 GC 的依据\n│   └── Mark Word  —— 运行时状态：锁 / GC 年龄 / hashCode\n├── 实例数据        —— 字段值本身\n└── 对齐填充        —— 补齐到 8 字节整数倍\n```\n\n对象头是\"自描述\"的固定成本（64 位压缩指针下约 12 字节）。两条稳定权衡：\n\n- **类型指针 = 数据与类型解耦**：对象只存字段值，类结构集中在方法区一份、由指针共享，避免每个对象重复携带。\n- **对齐填充 = 空间换访存效率**：按 CPU 字长对齐，让字段访问落在单次内存事务内；浪费几字节，换稳定性能。\n\n**Mark Word——定长空间的位复用**，是这里最本质的模式：\n\n| 位复用 | 收益 | 代价 |\n| --- | --- | --- |\n| 锁 / GC 年龄 / hashCode 共用同一片固定位 | 元信息内嵌对象头、就近可取、零额外指针 | 彼此互斥，须状态机分时占用 |\n\n> 这是一切\"元数据内联定长槽\"的通用权衡——指针压缩、tagged pointer、NaN-boxing 同理。**锁升级**不过是这片位在\"无竞争 → 交替 → 真竞争\"三种假设下的状态机（机制属并发领域，见 [JAVA并发编程](/编程语言/JAVA/JAVA并发编程/JAVA并发编程.md)\n\n### 定位：间接层的经典权衡\n\n引用如何找到对象，是\"要不要引入一层间接\"的通用抉择：\n\n| 方式 | 机制 | 得 | 失 |\n| --- | --- | --- | --- |\n| 句柄 | 引用 → 句柄池 → 对象 | 对象移动只改句柄，引用稳定 | 多一次解引用 |\n| 直接指针 | 引用 → 对象 | 少一次访存 | 对象移动须改所有引用 |\n\nHotSpot 选**直接指针**——把\"GC 移动时更新引用\"的成本，换访问热路径的速度。\n\n> 这是架构中反复出现的 **introduce-indirection** 模式：加一层间接换灵活 / 稳定（OS 虚拟地址、DNS、Handle），去一层换性能。定位之争只是它在对象访问上的落地。\n\n## 内存问题的本质：不是\"堆满了\"，而是\"某片内存触顶\"\n\n> OOM 不是一种错误，而是一族。每个 detail message 对应**不同的区、不同的根因、不同的排查路径**。而这些区，远不止堆。\n\n### 进程内存全景：Xmx 只管一行\n\n承接前文\"规范 vs 实现\"：分区树画的是**规范数据区**；进程**实际吃多少内存**是另一维度。JVM 进程内存只分两类——\n\n| 类别 | 成员 | `-Xmx` 管辖? |\n| ---- | ---- | ---------- |\n| 堆内 | Java 堆 | 是 |\n| 堆外(native) | Metaspace、线程栈、直接内存、Code Cache、GC 结构… | **否** |\n\n> 堆内只有堆一项，堆外是一批。每个堆外区各有自己的上限与 OOM 信号（如 Metaspace 撑爆、线程栈耗尽 native 内存），撑爆任一区都是 OOM，但**都不受 `-Xmx` 管辖**。\n> 故进程 RSS ≈ 堆 + Σ 堆外区；观测堆外全景靠 NMT，而非看堆的工具。\n\n### 两道天花板：内部上限与外部上限\n\n内存约束来自两个**彼此独立、互不知情**的执行者：\n\n- **JVM 内部上限**：某区达到 JVM 自设的界，JVM 主动抛 OOME——有异常、有现场，是\"体面的死\"。\n- **进程外部上限**：进程总内存触及 OS / 容器的界，被内核从外部杀——无异常、无现场，是\"静默的死\"。\n\n> 正因堆外区逃逸 `-Xmx`，外层天花板可能在**堆远未满**时先触发——这就是\"堆没满进程却被杀\"的根源，也是容器额度必须比 `-Xmx` 留出堆外余量的原因。诊断的第一步永远是分清：这次是**谁**、撞了**哪道**天花板。\n\n### 故障方法论\n\n现象 → 区域定位 → 根因\n\n```\n读 detail message，判归属\n  ├─ 堆内（heap space / GC overhead）→ heap dump + MAT，看 dominator tree / 引用链\n  ├─ 堆外（Metaspace / native thread / Direct buffer …）\n  │     → NMT baseline → 增长后 summary.diff，看哪个分类持续 +committed\n  └─ 无 message 但退出 137 → 非 JVM OOME，核对进程 RSS vs 容器 limit\n```\n\n## 总结：执行模型驱动的必然结果\n\nJVM 内存结构不是随意划分，而是\"字节码执行 + 多线程并发\"两条约束推导出的必然产物：\n\n```\n字节码执行模型 → 需物化执行现场 → PC / 栈\n多线程并发     → 需隔离与共享   → 私有区 / 共享区\n        └─ 共享区的存在，反过来定义了 GC 与 JMM 要治理的对象\n```\n\n**故理解内存结构的入口，是\"为什么需要这些区域\"，而非\"有哪些区域\"**——分区图是推导的结果，不是起点。\n\n## 关联内容（自动生成）\n\n- [/编程语言/JAVA/JVM/JVM.md](/编程语言/JAVA/JVM/JVM.md) JVM 整体架构与\"规范唯一、实现多样\"的判断，内存结构是其在数据区层的投影\n- [/编程语言/JAVA/JVM/自动内存管理/垃圾回收.md](/编程语言/JAVA/JVM/自动内存管理/垃圾回收.md) 分代/元模式、堆的回收算法与本篇堆结构互为表里\n- [/编程语言/JAVA/JVM/字节码执行引擎.md](/编程语言/JAVA/JVM/字节码执行引擎.md) 字节码执行引擎驱动栈帧的压入弹出，是\"栈支撑执行模型\"的运行时载体\n- [/编程语言/JAVA/JVM/字节码.md](/编程语言/JAVA/JVM/字节码.md) 栈式字节码指令集是操作数栈、栈帧存在的根本动因\n- [/编程语言/JAVA/JVM/类加载机制.md](/编程语言/JAVA/JVM/类加载机制.md) 方法区存储的类元数据由类加载写入，对象创建首步即类加载检查\n- [/编程语言/JAVA/JVM/JAVA内存模型.md](/编程语言/JAVA/JVM/JAVA内存模型.md) JMM 治理的正是本篇\"共享区\"的可见性与有序性，私有区天然无竞争\n- [/编程语言/JAVA/JAVA并发编程/JAVA并发编程.md](/编程语言/JAVA/JAVA并发编程/JAVA并发编程.md) 锁升级是 Mark Word 复用位在不同竞争假设下的状态机，本篇引流其机制细节至此\n- [/编程语言/JAVA/JAVA并发编程/线程.md](/编程语言/JAVA/JAVA并发编程/线程.md) 线程模型与线程私有区（虚拟机栈、程序计数器）的隔离设计直接对应\n- [/编程语言/JAVA/高级/NIO.md](/编程语言/JAVA/高级/NIO.md) 直接内存的 DirectByteBuffer 与 Zero Copy 是 NIO 的核心机制\n- [/操作系统/内存管理.md](/操作系统/内存管理.md) OS 内存管理（虚拟地址、text 段类比、native heap）与 JVM 内存管理在原理上同源\n","metadata":"tags: ['编程语言', '操作系统', '执行模型', '并发编程']","hasMoreCommit":true,"totalCommits":25,"commitList":[{"date":"2026-07-08T18:30:03+08:00","author":"MY","message":"docs(JVM): 重构JVM内存结构文档内容","hash":"d63044977a07cc4b2807313141fbd79ba07c35c0"},{"date":"2026-06-25T21:22:50+08:00","author":"MY","message":"refactor(java并发): 退役 基础概念.md，三处增量迁入权威篇","hash":"cc9e3a8038d179371125a1151bb670322796c083"},{"date":"2026-02-12T14:07:03+08:00","author":"MY","message":"doc: 整理标签","hash":"290b3e8ad18f48832ac282290238d020fc030a88"},{"date":"2026-01-15T17:44:23+08:00","author":"MY","message":"docs(JVM): 更新JVM内存结构文档内容","hash":"ea2f7ed08f7a6c2374c85647181abe33c82885ca"},{"date":"2025-09-21T14:03:43+08:00","author":"MY","message":"docs(mindmap): 统一思维导图根节点格式","hash":"44fc90fa0f22040d171dbf83cd6f2fd8c020444a"},{"date":"2025-01-13T13:37:42+08:00","author":"MY","message":"📦JVM 内存结构","hash":"4732c339293308b9d484462a0aa6994c52a237c9"},{"date":"2024-11-18T18:25:45+08:00","author":"MY","message":"📦JVM 内存结构","hash":"1aaa980dac5a310bb3a42d28c6d7e4ef54b9d2d0"},{"date":"2023-12-11T15:00:28+08:00","author":"MY","message":"📦内存结构","hash":"84798ac34a724ea332b4f2afbbf941472e78850e"},{"date":"2023-08-21T20:12:20+08:00","author":"MY","message":"✏JVM","hash":"593f0284b1b968f0d8db08aceae7d6cdef1f059f"},{"date":"2021-03-28T16:00:24+08:00","author":"MY","message":"✏更新 JVM 内存结构","hash":"ba408ef56453c3de9bbc2dbf7b458612a05103b9"}],"createTime":"2019-11-22T22:35:23+08:00"}