JVM 对象创建与内存布局
2025/12/21大约 8 分钟约 2369 字
基于 JDK 8+ 的 JVM 对象创建过程与内存布局深度解析。
一、对象创建的 5 个步骤

当执行 new Demo() 时,JVM 会依次执行以下步骤:
new Demo()
│
┌─────────────┴─────────────┐
│ 1. 类加载检查 │
├───────────────────────────┤
│ 2. 分配内存 │
├───────────────────────────┤
│ 3. 初始化零值 │
├───────────────────────────┤
│ 4. 设置对象头 │
├───────────────────────────┤
│ 5. 执行 <init> 方法 │
└───────────────────────────┘
│
▼
对象创建完成二、步骤详解
2.1 类加载检查
new Demo
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 从当前类的运行时常量池中找到 Demo 的符号引用 │
│ │
│ 符号引用已解析? │
│ 是 → 直接拿到 Demo 的 Klass 地址 │
│ 否 → 触发解析: │
│ 1. 查系统字典,Demo 加载了吗? │
│ 没有 → 触发类加载(加载→链接→初始化) │
│ 有了 → 获取 Klass 地址 │
│ 2. 更新常量池,符号引用 → 直接引用 │
│ │
│ 结果:拿到 Demo 的 Klass 地址 │
└──────────────────────────────────────────────────────────────┘关键理解:
- 类加载是对象创建的前提
- Klass 中存储了对象大小、字段偏移量等信息,创建对象必须先有 Klass
2.2 分配内存
从 Klass 中读取对象需要的内存大小,然后在堆中分配。

2.2.1 两种分配方式
| 分配方式 | 适用场景 | 原理 | 使用的收集器 |
|---|---|---|---|
| 指针碰撞 | 内存规整 | 移动分界指针 | Serial、ParNew、G1 |
| 空闲列表 | 内存不规整 | 维护空闲块列表 | CMS |
【指针碰撞 (Bump the Pointer)】
已使用 空闲
┌──────────────────┬─────────────────────────────────┐
│ A B C │ │
└──────────────────┴─────────────────────────────────┘
↑
分界指针
分配时:指针向空闲方向移动对象大小的距离
【空闲列表 (Free List)】
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ 空 │ C │ 空 │ E │ 空 │ 空 │ H │ 空 │ J │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
空闲列表: [1, 16B] → [3, 16B] → [5, 32B] → [8, 16B]
分配时:从列表中找到合适大小的空闲块2.2.2 并发安全问题
多个线程同时创建对象,可能出现指针冲突。
解决方案一:CAS + 失败重试
1. 读取当前指针值
2. 计算新指针值
3. CAS 更新指针
4. 失败则重试解决方案二:TLAB(Thread Local Allocation Buffer)
Eden 区
┌───────────────────────────────────────────────────────────────────┐
│ ┌─────────────┐ ┌─────────────┐ ┌───────────────────────┐ │
│ │ TLAB-A │ │ TLAB-B │ │ 公共区域 │ │
│ │ (线程A专用) │ │ (线程B专用) │ │ │ │
│ └─────────────┘ └─────────────┘ └───────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
- 每个线程预先申请一块 Eden 区的私有内存
- 线程在自己的 TLAB 中分配,无需同步
- TLAB 用完再申请新的(此时需要同步)
- 对象太大放不下 TLAB,直接在公共区域分配TLAB 相关参数:
| 参数 | 说明 |
|---|---|
-XX:+UseTLAB | 启用 TLAB(默认开启) |
-XX:TLABSize=512k | 设置 TLAB 大小 |
2.3 初始化零值
将分配到的内存空间(除对象头外)初始化为零值。
| 类型 | 零值 |
|---|---|
| int | 0 |
| long | 0L |
| double | 0.0 |
| float | 0.0f |
| boolean | false |
| char | '\u0000' |
| 引用类型 | null |
作用:保证实例变量不显式赋值也能使用默认值。
class Demo {
int count; // 不显式赋值
String name; // 不显式赋值
}
Demo demo = new Demo();
System.out.println(demo.count); // 输出 0
System.out.println(demo.name); // 输出 null2.4 设置对象头
在对象头中填充必要信息。
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 对象头 (Object Header) │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────────────┐ │
│ │ Mark Word (8 bytes) │ │
│ │ - HashCode(延迟计算,初始为 0) │ │
│ │ - GC 分代年龄 = 0 │ │
│ │ - 锁标志位 = 01(无锁) │ │
│ │ - 偏向锁标志 │ │
│ ├───────────────────────────────────────────────────────────────────────────────┤ │
│ │ Klass Pointer (4/8 bytes) │ │
│ │ 指向元空间的 Demo Klass │ │
│ ├───────────────────────────────────────────────────────────────────────────────┤ │
│ │ 数组长度 (4 bytes) - 仅数组对象有 │ │
│ └───────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘设置 Klass Pointer 的作用:
- JVM 知道对象属于哪个类
obj.getClass()能找到 Class 对象- 调用方法时能找到方法入口
2.5 执行 <init> 方法
从 JVM 角度,对象已创建完成。从 Java 角度,还需执行构造方法。
<init> 方法的执行顺序:
1. 调用父类的 <init>(递归到 Object)
│
▼
2. 实例变量赋值
int age = 18; → age 从 0 变成 18
│
▼
3. 实例代码块
{ System.out.println("实例代码块"); }
│
▼
4. 构造方法体
Demo() { this.name = "default"; }示例:
class Demo {
int age = 18;
{
System.out.println("实例代码块");
}
Demo() {
System.out.println("构造方法");
}
}
// new Demo() 输出:
// 实例代码块
// 构造方法三、对象的内存布局

对象在堆中的存储分为三个部分:
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 对象内存布局 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────────────┐ │
│ │ 对象头 (Object Header) │ │
│ │ - Mark Word 8 bytes │ │
│ │ - Klass Pointer 4/8 bytes(开启压缩 4 字节) │ │
│ │ - 数组长度(仅数组) 4 bytes │ │
│ └───────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────────────┐ │
│ │ 实例数据 (Instance Data) │ │
│ │ 存储对象的字段内容(包括父类继承的字段) │ │
│ │ 相同宽度的字段分配在一起 │ │
│ │ 父类字段在子类字段之前 │ │
│ └───────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────────────┐ │
│ │ 对齐填充 (Padding) │ │
│ │ HotSpot 要求对象大小必须是 8 字节的整数倍 │ │
│ └───────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘四、Mark Word 详解
Mark Word 是对象头中最复杂的部分,存储对象自身的运行时数据。

4.1 64 位 JVM Mark Word 结构
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ Mark Word (64 bits) │
├───────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【无锁态】 │
│ ┌──────────────────────────────┬──────────┬────────┬────────┬──────┐ │
│ │ HashCode (31 bits) │ unused │ age │ 偏向=0 │ 01 │ │
│ └──────────────────────────────┴──────────┴────────┴────────┴──────┘ │
│ │
│ 【偏向锁】 │
│ ┌───────────────────────────────────────┬───────┬────────┬────────┬──────┐ │
│ │ 线程ID (54 bits) │ Epoch │ age │ 偏向=1 │ 01 │ │
│ └───────────────────────────────────────┴───────┴────────┴────────┴──────┘ │
│ │
│ 【轻量级锁】 │
│ ┌─────────────────────────────────────────────────────────────────┬──────┐ │
│ │ 指向栈中锁记录的指针 (62 bits) │ 00 │ │
│ └─────────────────────────────────────────────────────────────────┴──────┘ │
│ │
│ 【重量级锁】 │
│ ┌─────────────────────────────────────────────────────────────────┬──────┐ │
│ │ 指向 Monitor 的指针 (62 bits) │ 10 │ │
│ └─────────────────────────────────────────────────────────────────┴──────┘ │
│ │
│ 【GC 标记】 │
│ ┌─────────────────────────────────────────────────────────────────┬──────┐ │
│ │ 空 │ 11 │ │
│ └─────────────────────────────────────────────────────────────────┴──────┘ │
│ │
└───────────────────────────────────────────────────────────────────────────────────────┘4.2 锁标志位对照表
| 锁状态 | 标志位 | 存储内容 |
|---|---|---|
| 无锁 | 01 | HashCode + GC 年龄 |
| 偏向锁 | 01 | 线程ID + Epoch + GC 年龄 + 偏向=1 |
| 轻量级锁 | 00 | 栈中锁记录指针 |
| 重量级锁 | 10 | Monitor 指针 |
| GC 标记 | 11 | 空 |
4.3 HashCode 与偏向锁的冲突
无锁态 Mark Word:存储 HashCode
偏向锁 Mark Word:存储线程 ID
两者冲突!
解决方案:
1. 如果对象已计算过 HashCode,无法进入偏向锁状态
2. 如果对象处于偏向锁状态,调用 hashCode() 会撤销偏向锁五、指针压缩
5.1 什么是指针压缩
64 位 JVM 中,对象指针占 8 字节。开启指针压缩后,用 4 字节存储。
| 参数 | 作用 |
|---|---|
-XX:+UseCompressedOops | 压缩普通对象指针(默认开启) |
-XX:+UseCompressedClassPointers | 压缩类指针(默认开启) |
5.2 压缩原理
未压缩:0x00000007 12345678 (8 字节)
压缩后存储:0x12345678 (4 字节)
解压时:0x12345678 << 3 + 堆基址 = 实际地址
因为对象按 8 字节对齐,低 3 位始终为 0,可以省略5.3 压缩限制
| 堆大小 | 是否可压缩 |
|---|---|
| < 4GB | 可以,且无需偏移 |
| 4GB ~ 32GB | 可以,需要偏移计算 |
| > 32GB | 不可以压缩 |
注意:堆超过 32GB 时,指针压缩失效,每个指针多占 4 字节!
六、对象的访问定位
JVM 通过栈上的 reference 访问堆中对象,有两种方式:
6.1 句柄访问
栈 句柄池 堆
┌─────────┐ ┌─────────────────┐ ┌─────────────┐
│ ref ───┼─────────►│ 对象实例指针 ───┼─────────►│ 对象实例 │
│ │ │ 类型数据指针 ───┼───┐ └─────────────┘
└─────────┘ └─────────────────┘ │
│ 元空间
│ ┌─────────┐
└─────►│ Klass │
└─────────┘
优点:对象移动时只需修改句柄,reference 不变
缺点:两次指针访问,效率较低6.2 直接指针(HotSpot 使用)
栈 堆
┌─────────┐ ┌─────────────────────┐
│ ref ───┼─────────────────►│ 对象实例 │
│ │ │ ┌───────────────┐ │
└─────────┘ │ │ Klass Pointer │──┼───► Klass (元空间)
│ └───────────────┘ │
└─────────────────────┘
优点:一次指针访问,效率高
缺点:对象移动时需修改 reference七、逃逸分析
7.1 什么是逃逸分析
分析对象的作用域,判断对象是否会"逃逸"出方法或线程。
// 不逃逸 - 对象只在方法内使用
public void method() {
Object obj = new Object();
// 使用 obj
}
// 会逃逸 - 返回给外部
public Object method() {
return new Object();
}
// 会逃逸 - 赋值给成员变量
public void method() {
this.field = new Object();
}7.2 基于逃逸分析的优化
| 优化技术 | 说明 | 条件 |
|---|---|---|
| 栈上分配 | 对象在栈上分配,方法结束自动销毁 | 对象不逃逸 |
| 标量替换 | 将对象拆解为标量,用局部变量存储 | 对象不逃逸 |
| 锁消除 | 去掉不必要的同步 | 对象不逃逸到其他线程 |
7.3 示例
public void method() {
Point p = new Point(1, 2); // 不逃逸
int x = p.x;
int y = p.y;
}
// 逃逸分析 + 标量替换后:
public void method() {
int p_x = 1; // 标量替换,不需要创建对象
int p_y = 2;
int x = p_x;
int y = p_y;
}相关参数:
| 参数 | 说明 |
|---|---|
-XX:+DoEscapeAnalysis | 开启逃逸分析(默认开启) |
-XX:+EliminateAllocations | 开启标量替换(默认开启) |
-XX:+EliminateLocks | 开启锁消除(默认开启) |
八、<clinit> vs <init> 对比
<clinit> | <init> | |
|---|---|---|
| 含义 | 类构造器 | 实例构造器 |
| 触发时机 | 类加载初始化阶段 | new 对象时 |
| 执行次数 | 每个类只执行一次 | 每次 new 都执行 |
| 处理内容 | 静态变量赋值 + 静态代码块 | 实例变量赋值 + 实例代码块 + 构造方法 |
class Demo {
// <clinit> 处理
static int count = 10;
static { System.out.println("静态代码块"); }
// <init> 处理
int age = 18;
{ System.out.println("实例代码块"); }
Demo() { System.out.println("构造方法"); }
}
Demo d1 = new Demo();
Demo d2 = new Demo();
// 输出:
// 静态代码块 ← 只执行一次
// 实例代码块 ← d1
// 构造方法 ← d1
// 实例代码块 ← d2
// 构造方法 ← d2九、常见面试问题
| 问题 | 要点 |
|---|---|
| 对象创建的步骤? | 类加载检查 → 分配内存 → 初始化零值 → 设置对象头 → 执行 init |
| 两种内存分配方式? | 指针碰撞(内存规整)、空闲列表(内存不规整) |
| 如何解决并发分配问题? | CAS + 失败重试、TLAB |
| 对象的内存布局? | 对象头 + 实例数据 + 对齐填充 |
| Mark Word 存什么? | HashCode、GC 年龄、锁状态、线程 ID 等 |
| 什么是指针压缩? | 用 4 字节存储 8 字节指针,堆 < 32GB 时有效 |
| 两种对象访问方式? | 句柄访问、直接指针(HotSpot 使用) |
| 什么是逃逸分析? | 分析对象是否逃出方法/线程,优化分配 |
<clinit> 和 <init> 区别? | 类初始化 vs 实例初始化 |
