---
title: JVM 内存模型与对象生命周期
type: concept
domain: java-concurrency
tags: [jvm, memory-model, heap, stack, escape-analysis, off-heap, gc-roots]
status: learning
created: 2026-09-22
last_reviewed: 2026-09-22
method: feynman
related_questions:
  - "Java中的对象一定在堆上分配内存吗？"
  - "JVM如何判断对象是否存活？"
related_knowledge:
  - ./jvm-reference-types.md
  - ./jvm-gc-algorithms.md
anki_cards: 5
interview_rounds:
  - "2026-09-22-round-1-Q1"
  - "2026-09-22-round-1-Q3"
---

# JVM 内存模型与对象生命周期

> 对象去哪分配、怎么被判定存活。核心是"堆栈堆外三种位置 + 逃逸分析 + 可达性分析 + GC Roots"。

## TL;DR（30 秒扫完）

- Java 对象**默认在堆上**，但逃逸分析+JIT 可标量替换（消除对象）；堆外是**主动使用**不是自动迁移
- 存活判定用**可达性分析**（从 GC Roots 遍历），不用引用计数（循环引用死锁）
- GC Roots = 栈局部变量 + 静态字段 + 常量池 + synchronized 锁对象 + JNI 引用 + 类加载器 + JVM 内部
- 对象"不可达"≠"已死"：`finalize()` 可自救（JDK 9 弃用改 Cleaner）
- 弱分代假说：几乎所有对象朝生夕死，熬过几次 GC 的对象更难死

## 关键结论

- **结论 A**：HotSpot 更常见的是**消除对象**（标量替换）而非把对象搬到栈；"栈上分配"是修辞
- **结论 B**：堆外内存（DirectByteBuffer/Unsafe/Netty PooledAllocator）是独立机制，GC 只回收引用包装对象
- **结论 C**：GC 判定存活 = 可达性分析；引用计数因循环引用被 Java/Python/JS 全部放弃
- **结论 D**：分代设计的理论基础是**弱分代假说**——几乎所有对象朝生夕死

## 完整讲解（费曼四步）

### STEP 1 · 概念

JVM 内存模型规定对象语义上属于托管对象，默认在**堆**上分配。但 JIT 通过逃逸分析+标量替换，可在特定条件下不产生堆分配。堆外内存是另一套独立于 Java 堆的原生内存，需要开发者主动使用。

### STEP 2 · 大白话

> **类比**：栈上分配像是"临时工"，做完就走；堆分配像"正式员工"，由公司（GC）管理；堆外像"外包"，你要自己管薪资（释放）。JIT 优化的精髓是让"正式员工"直接以"临时工"的方式干活——把对象拆成局部变量，连"入职手续"（堆分配）都省了。

### STEP 3 · 底层

#### 对象分配位置

| 位置 | 何时分配 | 谁释放 | 典型载体 |
|------|---------|--------|---------|
| 堆 | 默认路径，`new` | GC（TLAB 指针碰撞） | 绝大多数对象 |
| 栈（标量替换） | 逃逸分析判定不逃逸 | 方法出栈自动清理 | JIT 优化后的短命对象 |
| 堆外 | 开发者主动申请 | 显式释放 / Cleaner | DirectByteBuffer、Netty PooledByteBufAllocator |

**逃逸分析三分类**：

| 分类 | 场景 | JIT 处理 |
|------|------|---------|
| 不逃逸 | 只在创建方法内使用 | 标量替换（首选）/ 栈分配 |
| 线程逃逸 | 跨方法调用不出线程 | TLAB 快速路径 |
| 堆逃逸 | 引用保存到全局/返回其他线程 | 必须堆分配 |

JDK 9+ 逃逸分析默认开启（`-XX:+DoEscapeAnalysis`），标量替换配套 `-XX:+EliminateAllocations`。

#### 存活判定：两种算法

| 算法 | 机制 | 缺陷 | 使用者 |
|------|------|------|--------|
| 引用计数 | 每个对象维护计数器，+1/-1，为 0 死 | **循环引用无法回收**（A→B→A） | Obj-C ARC（靠 weak 破环）、Go 早期（弃用） |
| 可达性分析 | 从 GC Roots 遍历引用链 | 需 STW 或并发标记 | Java / Python / JS V8 |

#### GC Roots 完整清单（HotSpot）

1. **栈引用**：活跃线程的栈帧局部变量、方法参数、运算栈
2. **静态字段**：类的 `static` 变量（含 `static final` 引用）
3. **常量池引用**：运行时常量池的类引用
4. **`synchronized` 持有的对象**（监视器锁对象）
5. **JNI 引用**：本地方法持有的 Java 对象
6. **类加载器** + **JVM 内部引用**（GC 实现细节、Unreachable class 元数据）

#### "不可达" ≠ "已死"

- 若重写 `finalize()` 且未调用过 → GC 放到 F-Queue，"自救线程"执行
- 在 `finalize()` 中重新赋值可回到可达（**只有一次机会**）
- JDK 9+ 弃用 `finalize`（性能差、时机不确定），改用 **Cleaner**（虚引用+ReferenceQueue）

### STEP 4 · 简化

> **一句话总结**：对象默认堆上，JIT 可能消除，堆外自己管；存活判定用可达性，从 GC Roots 出发；分代设计建立在"朝生夕死"假说上。

**记忆口诀**：
- **堆为主，标量为辅，堆外自理**
- **可达性 > 引用计数（循环引用是坑）**
- **GC Roots 六项：栈、静、常、锁、JNI、类加载**

## 常见误区

- **误区 1**："JVM 会自动把大对象搬到堆外" → 正确：堆外是独立机制，需要主动使用 DirectByteBuffer/Unsafe/Netty PooledAllocator
- **误区 2**："栈上分配就是对象真的放在栈帧里" → 正确：HotSpot 更常见是**标量替换**——把对象字段拆成寄存器/栈变量
- **误区 3**："引用计数 Java 也能用" → 正确：循环引用 A→B→A 无法回收，Java/Python/JS 全部放弃
- **误区 4**："GC Roots 就是静态字段+局部变量" → 正确：漏了 `synchronized` 锁对象、JNI 引用、类加载器、JVM 内部
- **误区 5**："不可达 = 已死" → 正确：`finalize()` 可自救；JDK 9+ 改 Cleaner

## 延伸追问

1. **逃逸分析什么时候失效？**
   - 对象被同步（`synchronized this`）、反射访问、赋值给 `volatile`、序列化、保存到静态字段、接口调用无法确定最终类型时，JIT 保守放弃标量替换
2. **DirectByteBuffer 如何避免泄漏？**
   - JDK 9+ 用 Cleaner 自动回收；JDK 8 靠 PhantomReference + 手动 clean；监控 `-XX:MaxDirectMemorySize`、`jcmd VM.native_memory`、JFR `jdk.DirectByteBuffer` 事件
3. **如何用 MAT 排查内存泄漏？**
   - Dump 堆 → MAT 打开 → Dominator Tree 找大对象持有者 → Shortest Path to GC Roots 看是不是"不该被引用的强引用"
4. **弱分代假说对 GC 策略的影响？**
   - 新生代小快（复制算法），老年代慢少（标记-整理）——这就是"为什么分代"的答案
5. **finalize() 为什么被弃用？**
   - 执行时机不确定、性能差（每次 GC 前要跑）、可能"复活"对象；JDK 9 标记 deprecated，改 Cleaner

## 速查表

```
对象位置: 堆（默认）/ 栈（标量替换，非真栈）/ 堆外（主动）
逃逸分析: 不逃逸→标量替换 / 线程逃逸→TLAB / 堆逃逸→堆分配
存活判定: 可达性（GC Roots 出发）> 引用计数（循环引用死锁）
GC Roots: 栈 + 静态字段 + 常量池 + synchronized + JNI + 类加载器
自救机制: finalize（弃用）→ Cleaner（JDK 9+）
分代基础: 弱分代假说（朝生夕死）
```

## 关联题目（题库）

- 《Java中的对象一定在堆上分配内存吗？》— `00java/03JVM/03对象创建与内存分配/`, Round 1 Q1, ⭐⭐⭐
- 《JVM如何判断对象是否存活？》— `00java/03JVM/04垃圾回收（GC）基础/`, Round 1 Q3, ⭐⭐⭐

## 关联知识

- [四种引用类型](./jvm-reference-types.md)
- [JVM GC 算法](./jvm-gc-algorithms.md)
- [JVM GC 器选型](./jvm-gc-select-tune.md)
- [主题地图](./_moc.md)

## Anki 候选卡片

1. **正**：HotSpot 栈上分配的本质是什么？**反**：标量替换（把对象字段拆成寄存器/栈变量），不是对象字面放到栈帧
2. **正**：GC Roots 六项清单？**反**：栈局部变量 / 静态字段 / 常量池 / synchronized 锁对象 / JNI 引用 / 类加载器+JVM 内部
3. **正**：为什么 Java 不用引用计数？**反**：循环引用 A→B→A 无法回收
4. **正**：DirectByteBuffer 泄漏怎么查？**反**：JDK 9+ Cleaner 自动、JDK 8 PhantomReference；`jcmd VM.native_memory`、JFR `jdk.DirectByteBuffer`
5. **正**：弱分代假说是什么？**反**：几乎所有对象朝生夕死，熬过几次 GC 的对象更难死

---

*最后更新：2026-09-22*
