一句话总结
Java 对象创建分五步:① 类加载检查(检查类是否已加载)→ ② 分配内存(指针碰撞或空闲列表,通过 CAS+失败重试保证线程安全,或使用 TLAB 减少竞争)→ ③ 初始化零值(将分配的内存空间初始化为零值)→ ④ 设置对象头(Mark Word + 类型指针)→ ⑤ 执行 <init>(构造函数,按父类到子类的顺序执行)。
初级理解
对象创建五步流程
// new Object() 的完整过程
Object obj = new Object();
// ① 类加载检查
// 检查 Object 类是否已加载,未加载则先执行类加载
// ② 分配内存
// 在堆中为新对象分配一块内存空间
// 分配方式:指针碰撞(规整堆)或 空闲列表(碎片堆)
// ③ 初始化零值
// 将分配的内存空间初始化为零值(不包括对象头)
// int → 0, boolean → false, 引用 → null
// ④ 设置对象头
// 设置 Mark Word(哈希码、GC 分代年龄、锁状态等)
// 设置类型指针(指向方法区中的类元数据)
// ⑤ 执行 <init>
// 执行构造函数,包括实例变量初始化、实例代码块、构造方法
// 按父类 → 子类的顺序执行
对象内存布局
// 一个 Java 对象在堆中的内存布局(64位 JVM,压缩指针开启)
┌──────────────────────────────────────┐
│ 对象头 Header │
├──────────────────────────────────────┤
│ Mark Word(8 字节) │
│ - 哈希码(25 bit) │
│ - GC 分代年龄(4 bit) │
│ - 偏向锁标记(1 bit) │
│ - 锁标志位(2 bit) │
├──────────────────────────────────────┤
│ 类型指针 Klass Pointer(4 字节压缩) │
│ 指向方法区中的 Class 元数据 │
├──────────────────────────────────────┤
│ 数组长度(如果是数组,4 字节) │
├──────────────────────────────────────┤
│ 实例数据 Instance Data │
│ 按字段类型和长度排列 │
│ (相同宽度的字段分配在一起) │
├──────────────────────────────────────┤
│ 对齐填充 Padding │
│ 保证对象大小是 8 字节的整数倍 │
└──────────────────────────────────────┘
中级深入
内存分配的线程安全
// 问题:多个线程同时 new 对象,如何保证内存分配安全?
// 方案一:CAS + 失败重试
// 使用 CAS 操作更新内存分配指针,失败则重试
// 简单但竞争激烈时效率低
// 方案二:TLAB(Thread Local Allocation Buffer)
// 每个线程在 Eden 区预分配一块私有缓冲区(默认 Eden 的 1%)
// 线程在自己的 TLAB 中分配对象,无需同步
// TLAB 用完才需要申请新的 TLAB(需要同步)
// -XX:+UseTLAB(默认开启)
// -XX:TLABSize 设置 TLAB 大小
两种内存分配方式
| 方式 | 适用场景 | 原理 |
| 指针碰撞 | 堆内存规整(Serial、ParNew) | 已用内存和空闲内存中间有一个指针,分配时指针移动对象大小距离 |
| 空闲列表 | 堆内存不规整(CMS) | 维护一个空闲内存列表,分配时从列表中找到足够大的空间 |
对象访问定位
// ① 句柄访问(间接访问)
// 栈 → 句柄池 → 对象实例数据 + 对象类型数据
// 优点:对象移动时只需改句柄,引用稳定
// 缺点:多一次间接访问
// ② 直接指针访问(HotSpot 使用)
// 栈 → 对象实例数据(含类型指针)→ 对象类型数据
// 优点:访问速度快,少一次指针定位
// 缺点:对象移动时需要更新所有引用
高级拓展
压缩指针(Compressed Oops)
// 64 位 JVM 中,对象指针默认 8 字节
// 开启压缩指针后,指针压缩为 4 字节(堆 < 32GB)
// -XX:+UseCompressedOops(默认开启)
// -XX:+UseCompressedClassPointers(压缩类型指针)
// 原理:Java 对象 8 字节对齐,地址低 3 位始终为 0
// 存储时右移 3 位,使用时左移 3 位
// 4 字节 × 8 = 32GB 寻址范围
// 好处:减少内存占用,提高缓存命中率
字段重排序
HotSpot 会对实例字段进行重排序以减少内存浪费:相同宽度的字段分配在一起,按 double/long → int/float → short/char → byte/boolean → 引用的顺序排列。父类字段在子类字段之前。
实战场景
查看对象内存布局
// 使用 JOL(Java Object Layout)查看对象布局
// 依赖:org.openjdk.jol:jol-core
import org.openjdk.jol.info.ClassLayout;
public class ObjectLayout {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
// 输出示例(64位,压缩指针开启):
// OFFSET SIZE TYPE DESCRIPTION
// 0 4 (object header) // Mark Word 前 4 字节
// 4 4 (object header) // Mark Word 后 4 字节
// 8 4 (object header) // 类型指针(压缩后 4 字节)
// 12 4 (loss due to alignment) // 对齐填充
// Instance size: 16 bytes
面试模拟
Q:Java 对象创建的过程是怎样的?
A:五步:类加载检查→分配内存(指针碰撞/空闲列表,TLAB 保证线程安全)→初始化零值→设置对象头(Mark Word + 类型指针)→执行 <init>(构造函数,父类→子类)。
Q:对象在内存中的存储布局是怎样的?
A:对象头(Mark Word 8字节 + 类型指针 4/8字节 + 数组长度)→ 实例数据(字段按宽度排列,相同宽度放一起)→ 对齐填充(保证 8 字节对齐)。64 位 JVM 开启压缩指针后,普通对象头 12 字节,加 4 字节对齐填充共 16 字节。
Q:TLAB 是什么?
A:TLAB(Thread Local Allocation Buffer)是每个线程在 Eden 区预分配的私有缓冲区。线程在自己的 TLAB 中分配对象无需同步,减少 CAS 竞争。TLAB 用完才需要申请新的(需要同步)。默认开启,大小约为 Eden 的 1%。