04 - 对象分配与内存布局
定位: 从
new指令到对象销毁 —— JVM 中对象的完整生命周期 面试高频度: ⭐⭐⭐⭐⭐
一、对象生命周期在 JVM 体系中的位置
类加载完成后,JVM 拥有了某个类的运行时元数据(方法区中)。接下来才是 Java 程序真正核心的工作:创建对象并使用它们。
对象生命周期处于 JVM 运行流程的中枢:
- 向上: 接收类加载的结果,在堆上实例化出具体对象
- 向下: 对象最终被 GC 回收,释放堆空间
- 横向: 对象内存布局决定了 JIT 的优化空间(逃逸分析、标量替换)
┌─────────────────────────────────────────────────────────────────────┐
│ JVM 中对象的完整生命周期 │
│ │
│ ┌─────────┐ ┌──────────┐ ┌────────┐ ┌───────────────┐ │
│ │ 创建 │ │ 内存布局 │ │ 访问 │ │ 回收 │ │
│ │ │ │ │ │ │ │ │ │
│ │ new 指令 │──→ │ MarkWord │──→ │ 句柄 │──→ │ GC 判定不可达 │ │
│ │ 5步流程 │ │ KlassPtr │ │ 直接 │ │ 对象头 GC 标记 │ │
│ │ 内存分配 │ │ 实例数据 │ │ 指针 │ │ 空间被释放 │ │
│ │ 初始化 │ │ 对齐填充 │ │ │ │ │ │
│ └─────────┘ └──────────┘ └────────┘ └───────────────┘ │
│ ↑ ↑ ↑ ↑ │
│ 04.1 对象 04.2 对象 04.3 对象 05 GC 机制 │
│ 创建过程 内存布局 访问定位 对象回收 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 分配策略 (贯穿整个生命周期) │ │
│ │ TLAB · 栈上分配 · 逃逸分析 · 标量替换 · 大对象直接进老年代 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ↑ │
│ 04.4 分配策略 │
└─────────────────────────────────────────────────────────────────────┘二、子专题导航
| # | 主题 | 面试频率 | 核心内容 |
|---|---|---|---|
| **对象创建过程** | 对象创建过程 | ⭐⭐⭐⭐⭐ | 类加载检查 → 内存分配(指针碰撞/空闲列表) → CAS/TLAB → 零值初始化 → 对象头设置 → <init> 执行 |
| **对象内存布局** | 对象内存布局 | ⭐⭐⭐⭐⭐ | MarkWord 8字节(锁状态/hashCode/GC age)、Klass Pointer(压缩指针)、实例数据排序、对齐填充 |
| **对象访问定位** | 对象访问定位 | ⭐⭐⭐⭐ | 句柄访问 vs 直接指针访问(两种方式的优劣势对比)、HotSpot 的选择动机 |
| **分配策略** | 分配策略专题 | ⭐⭐⭐⭐⭐ | TLAB 并发分配、逃逸分析与栈上分配、标量替换、大对象直接进入老年代 |
三、核心考点速记
1. 对象创建的 5 步(最核心考点)
new 指令
│
▼
① 类加载检查 ─→ 检查常量池中是否有类的符号引用 → 检查类是否已加载/解析/初始化
│ 未加载 → 先执行类加载流程 ([[02.1-类加载生命周期]])
▼
② 分配内存 ───→ 指针碰撞 (GC 规整) 或 空闲列表 (GC 不规整)
│ 并发安全: TLAB (无锁) 或 CAS + 失败重试
▼
③ 初始化零值 → 实例字段设为 0/null (保证创建对象的字段可预期)
▼
④ 设置对象头 → MarkWord + Klass Pointer (+ 数组长度)
▼
⑤ 执行 <init> → 开发者编写的构造函数 (唯一可控步骤)2. MarkWord 结构 (64位 JVM)
无锁状态:
┌──────────────┬──────────────────────┬──────────┬──────┬─────────┬─────────┐
│ unused:25 │ identity_hashcode │ unused:1 │ age │ biased │ lock │
│ │ (31 bits) │ │ (4b) │ (1b) │ (2b) │
└──────────────┴──────────────────────┴──────────┴──────┴─────────┴─────────┘
↑
GC 年龄仅 4 bit → 最大值 15
→ MaxTenuringThreshold 上限为 153. TLAB vs 栈上分配
| 特性 | TLAB (Thread Local Allocation Buffer) | 栈上分配 (Stack Allocation) |
|---|---|---|
| 原理 | 线程在 Eden 内预留私有缓冲区 | 逃逸分析判定对象未逃逸 → 在栈帧或寄存器分配 |
| 同步开销 | TLAB 内零同步,TLAB 耗尽时 CAS | 完全不需要同步 |
| 回收方式 | GC 回收 (对象仍在堆上) | 方法返回时栈帧弹出即回收 (零 GC 开销) |
| 触发条件 | 默认开启 | C2 编译器 + 逃逸分析通过 (-XX:+DoEscapeAnalysis) |
| 效果 | 减少 CAS 竞争,高并发下 2-3 倍加速 | 消除大量临时对象的堆分配和 GC 压力 |
4. 对象访问方式对比
句柄访问: 直接指针访问 (HotSpot):
┌─────────┐ ┌─────────┐
│reference│ │reference│
└────┬────┘ └────┬────┘
▼ ▼
┌─────────────────────┐ ┌──────────────────┐
│ 句柄池 (Handle) │ │ 对象实例 │
│ ┌─────────────────┐│ │ ┌──────────────┐ │
│ │ 实例数据指针 ──────│──→ 对象 │ │ MarkWord │ │
│ │ 类型数据指针 ──────│──→ 方法区│ │ KlassPtr ────│─→ 方法区元数据
│ └─────────────────┘│ │ │ 实例数据 │ │
└─────────────────────┘ │ └──────────────┘ │
└──────────────────┘四、面试高频追问一览
| 追问 | 关联笔记 | 频次 |
|---|---|---|
| new 一个对象在 JVM 中发生了什么? | **对象创建过程** | 极高 |
| 对象在内存中的布局结构? | **对象内存布局** | 极高 |
| MarkWord 包含哪些信息? | **对象内存布局** | 极高 |
| 什么是压缩指针?为什么能压缩? | **对象内存布局** | 高 |
| 对象的 GC 年龄为什么最大 15? | **对象内存布局** | 高 |
| HotSpot 用哪种对象访问方式?为什么? | **对象访问定位** | 中高 |
| 什么是逃逸分析?什么是标量替换? | **分配策略** | 极高 |
| TLAB 是什么?为什么需要它? | **分配策略** | 高 |
| 什么对象会直接进入老年代? | **分配策略** | 中高 |
📚 相关链接
- **Java堆** — 堆的分代结构是对象分配的目标区域
- **方法区与元空间** — Klass 指针指向方法区类元数据
- **类加载生命周期** — 对象创建前必须先完成类加载
- **垃圾收集器详解** — 不同 GC 影响分配方式(指针碰撞 vs 空闲列表)
- **方法内联和逃逸分析** — JIT 逃逸分析的深入原理
- ← 返回 **JVM文库设计文档**