Java 对象创建过程,一般按 “类加载检查 → 分配内存 → 初始化零值 → 设置对象头 → 执行 <init>” 这条线来回答。
一、对象创建的完整过程
1. 类加载检查
执行 new 指令时,JVM 会先检查这个类是否已经被加载、解析和初始化。
如果没有,就先执行类加载过程:
- 加载
- 验证
- 准备
- 解析
- 初始化
也就是说,对象能不能创建,前提是类已经可用。
2. 为对象分配内存
类加载完成后,JVM 已经知道这个对象需要多大内存。
然后在堆中给对象分配空间。分配方式常见有两种:
-
指针碰撞
- 适用于内存规整的场景
- 用过的内存放一边,空闲内存放另一边,中间用指针分隔
- 分配时只需要移动指针
-
空闲列表
- 适用于内存不规整的场景
- JVM 维护一张空闲内存表,分配时从表中找到足够大的空间
是否规整,通常和 GC 使用的收集器是否带压缩整理 有关。
3. 处理并发安全问题
因为对象创建在多线程下可能同时发生,所以分配内存时要保证线程安全,常见方式:
-
CAS + 重试
-
TLAB(Thread Local Allocation Buffer)
- 给每个线程预先分配一小块私有缓冲区
- 线程优先在自己的 TLAB 中分配对象
- 减少锁竞争,提高分配效率
面试里提到 TLAB,通常会加分。
4. 初始化为零值
内存分配完成后,JVM 会先把对象实例数据区域初始化为零值:
int->0long->0Lboolean->false- 引用类型 ->
null
这一步保证对象字段即使不显式赋值,也有默认值。
注意:
这时还 不是程序员代码里写的初始化值,只是 JVM 默认零值。
5. 设置对象头
接下来 JVM 会设置对象头中的信息,例如:
- 这个对象属于哪个类
- 对象的哈希码
- GC 分代年龄
- 锁状态标志
- 偏向锁线程 ID(特定场景下)
如果是数组,对象头里还会记录数组长度。
6. 执行 <init> 方法
最后执行构造方法,也就是字节码层面的 <init>。
这一阶段才真正按代码逻辑进行初始化,比如:
User user = new User();
如果 User 类中写了:
private int age = 18;
public User() {
this.name = "Tom";
}
那么这些赋值操作都是在 <init> 中完成的。
这一步执行完,才算一个真正可用的对象创建完成。
二、可以概括成一句话
对象创建过程就是:先检查类是否已加载,然后在堆上分配内存,接着把实例字段设为默认零值,再设置对象头,最后执行构造方法 <init> 完成程序员定义的初始化。
三、面试高频补充点
1. 对象一定在堆上吗?
通常是,但不绝对。
在 JIT 优化下,可能发生:
- 逃逸分析
- 标量替换
- 栈上分配
所以有些对象未必真的落到堆里。
2. new 关键字做了什么?
可以回答:
- 找到类元信息
- 申请对象内存
- 默认值初始化
- 调用构造方法
- 返回对象引用
3. 成员变量初始化顺序
一个对象初始化时,通常顺序可理解为:
- 父类静态变量 / 静态代码块(类加载时,只执行一次)
- 子类静态变量 / 静态代码块
- 父类实例变量 / 实例代码块
- 父类构造方法
- 子类实例变量 / 实例代码块
- 子类构造方法
四、背诵版
Java 对象创建时,首先会检查对应的类是否已经完成加载、解析和初始化;如果没有,会先进行类加载。然后 JVM 在堆中为对象分配内存,分配时会通过 CAS 或 TLAB 保证线程安全。分配完成后,会先把对象实例字段初始化为零值,再设置对象头信息,例如类指针、哈希码、GC 年龄和锁标志等。最后执行构造方法 <init>,按照程序员写的逻辑完成成员变量赋值和对象初始化,这样一个对象才算真正创建完成。
五、简版口诀
类加载检查,分配内存,零值初始化,设置对象头,执行构造方法。