对象创建与内存布局
Java 中一句 new Object() 背后,JVM 做了类加载检查、内存分配、对象初始化等一系列动作。理解对象创建过程,有助于理解性能优化、锁、GC 和内存占用。
# 1. 对象创建过程
创建对象大致包括:
- 类加载检查。
- 分配内存。
- 初始化零值。
- 设置对象头。
- 执行构造方法。
User user = new User();
这行代码不是简单分配一块内存,还要保证类已经加载并完成必要初始化。
# 2. 内存分配方式
常见分配方式:
- 指针碰撞。
- 空闲列表。
如果堆内存规整,JVM 只需要移动指针即可分配对象,这就是指针碰撞。
如果堆内存不规整,JVM 需要维护可用内存块列表,从列表中找合适空间。
垃圾回收器是否整理内存,会影响分配方式。
# 3. TLAB
TLAB(Thread Local Allocation Buffer)是线程本地分配缓冲区。
多线程同时在堆上分配对象会产生竞争。JVM 会给每个线程在 Eden 中分配一小块私有区域,对象优先在线程自己的 TLAB 中分配。
优点:
- 减少分配锁竞争。
- 提升小对象分配效率。
TLAB 分配失败后,可能走慢路径分配。
# 4. 对象内存布局
HotSpot 中普通对象内存布局通常包括:
- 对象头。
- 实例数据。
- 对齐填充。
数组对象还会包含数组长度。
对象 = 对象头 + 实例字段 + 对齐填充
数组 = 对象头 + 数组长度 + 数组元素 + 对齐填充
# 5. 对象头
对象头通常包含:
- Mark Word。
- Klass Pointer。
Mark Word 中可能存放:
- 哈希码。
- GC 年龄。
- 锁状态。
- 偏向线程信息。
Klass Pointer 指向类元数据,用于知道这个对象属于哪个类。
# 6. 压缩指针
64 位 JVM 中,对象引用默认可能使用压缩指针,以减少内存占用。
相关参数:
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers
压缩指针对大多数应用有利,可以降低对象引用和类指针占用。
# 7. 对象一定在堆上吗
从语义上看,对象分配在堆上。但经过 JIT 优化后,如果对象没有逃逸,可能被标量替换,甚至不真正分配对象。
例如:
public int sum() {
Point p = new Point(1, 2);
return p.x + p.y;
}
如果 p 没有逃逸出方法,JIT 可能把对象拆成两个局部变量。
# 8. 阶段小结
对象创建涉及类加载、内存分配、零值初始化、对象头设置和构造方法执行。对象布局中的对象头、实例数据、对齐填充会影响内存占用。理解 TLAB 和逃逸分析,有助于理解 JVM 为什么能高效分配大量短生命周期对象。
# 9. 对象创建详细流程图
执行 new 指令
│
├─ 1. 检查类是否已加载
│ └─ 未加载则触发类加载
│
├─ 2. 计算对象所需内存大小
│
├─ 3. 尝试在 TLAB 中分配
│ ├─ 成功 -> 继续
│ └─ 失败 -> 进入慢路径分配
│
├─ 4. 初始化内存为零值
│
├─ 5. 设置对象头
│ ├─ Mark Word
│ └─ Klass Pointer
│
├─ 6. 执行 <init> 构造方法
│
└─ 7. 引用指向新对象
这里的“零值初始化”很关键,它保证 Java 字段即使没有显式赋值,也有确定默认值。
# 10. 对象布局示意
普通对象
┌─────────────────────┐
│ Mark Word │ hash、GC 年龄、锁状态等
├─────────────────────┤
│ Klass Pointer │ 指向类元数据
├─────────────────────┤
│ 实例字段 │ int、long、引用字段等
├─────────────────────┤
│ 对齐填充 │ 对齐到对象粒度
└─────────────────────┘
数组对象
┌─────────────────────┐
│ Mark Word │
├─────────────────────┤
│ Klass Pointer │
├─────────────────────┤
│ Array Length │
├─────────────────────┤
│ 数组元素 │
├─────────────────────┤
│ 对齐填充 │
└─────────────────────┘
对象对齐意味着一个对象的实际占用可能大于字段大小之和。
# 11. 字段排列和内存占用
JVM 可能对字段布局做优化,以减少对齐填充。但开发中仍要知道:
long、double通常占 8 字节。- 引用在开启压缩指针时通常占 4 字节。
- 对象头本身有固定开销。
- 大量小对象的对象头开销非常可观。
例如一个只包含一个 boolean 字段的对象,也不可能只占 1 字节。
# 12. 开发中常见问题
# 12.1 为什么大量小对象也会占很多内存
每个对象都有对象头和对齐填充。大量小对象的元数据开销可能超过字段本身。
优化方向:
- 使用基本类型数组。
- 合并对象。
- 避免过度拆分模型。
- 使用对象池要谨慎,现代 GC 对短命对象很擅长。
# 12.2 为什么 new 对象很快
大多数对象在 TLAB 中分配,通常只是移动指针,成本很低。真正昂贵的往往不是 new 本身,而是对象逃逸、长期存活、GC 压力和缓存未命中。
# 专家速查表
| 组成部分 | 内容 | 优化关注点 |
|---|---|---|
| 对象头 Mark Word | hash、锁状态、GC 年龄等 | 锁优化和对象状态 |
| 类型指针 | 指向 Klass 元数据 | 压缩类指针影响大小 |
| 实例数据 | 对象字段值 | 字段类型和排列影响占用 |
| 对齐填充 | 8 字节对齐补齐 | 小对象数量多时影响明显 |
| 引用字段 | 指向其他对象 | 对象图深度影响 GC |
对象大小不是字段大小简单相加,还受到对象头、引用压缩、字段排列和对齐影响。大量小对象场景中,减少对象数量往往比微调字段更有效。
# 13. Tips 快问快答
Q:对象头里一定有 hashCode 吗?
A:对象头 Mark Word 可以存 hashCode,但通常在真正调用 hashCode() 后才需要保存相关信息。
Q:数组对象为什么比普通对象多一块长度信息?
A:JVM 需要知道数组长度以支持边界检查和 array.length。
Q:开启压缩指针有什么好处?
A:减少引用字段占用,降低堆内存压力,提高缓存友好性。
Q:对象分配失败一定立刻 OOM 吗?
A:不一定。JVM 可能先触发 GC,GC 后仍无法分配才可能 OOM。