文章目录
- 一、全流程概览
- 二、第一关:类加载检查——JVM 认不认识你?
- 三、第二关:分配内存——给对象找个"房子"
- 并发安全:两个人抢同一间房怎么办?
- 四、第三关:零值初始化——毛坯房刷白墙
- 五、第四关:设置对象头——装门牌号和监控
- 对象头包含什么?
- 六、第五关:执行构造方法——精装修
- 完整执行顺序
- 七、回到全貌:一张图串起来
- 八、面试速答模板
个人网站
写代码时,new Person()一敲,对象就出来了——就像你在餐厅点个菜,菜就端上来了。但你有没有想过:厨房里到底经历了什么?食材采购、清洗切配、大火爆炒、摆盘装饰……你看到的只是最后一秒。
Java 对象的创建也是一样。你以为只是一句new,JVM 却在背后默默跑了五道流程,哪一道出了问题,你的对象都活不了。今天我们就去"后厨"看看,一个 Java 对象到底是怎么被造出来的。
一、全流程概览
一个对象从new到可用,要经过以下五步:
new Person() │ ├─ ① 类加载检查 → 你这个类 JVM 认识吗? ├─ ② 分配内存 → 给对象找个"房子" ├─ ③ 零值初始化 → 毛坯房刷白墙(字段设默认值) ├─ ④ 设置对象头 → 装门牌号和监控(HashCode、GC 分代年龄等) └─ ⑤ 执行构造方法 → 精装修(你写的赋值逻辑)下面逐步拆解,保证你看完就懂。
二、第一关:类加载检查——JVM 认不认识你?
当 JVM 遇到new Person(),第一件事不是分配内存,而是问:Person 这个类加载了吗?
就像你去酒店入住,前台先查你有没有预约记录。没有?对不起,先去办手续。
Personp=newPerson();JVM 会检查Person这个符号引用是否已在方法区中:
- 已加载→ 直接进入下一步
- 未加载→ 触发类加载过程(加载 → 验证 → 准备 → 解析 → 初始化),加载完再继续
这就是为什么有时候
new一个对象会触发一堆 static 代码块执行——类加载的初始化阶段会执行<clinit>()方法,里面包含了所有 static 变量赋值和 static 代码块。
classPerson{static{System.out.println("类加载了!");// new 之前会先执行}}newPerson();// 控制台输出:类加载了!三、第二关:分配内存——给对象找个"房子"
类加载通过后,JVM 要给对象分配一块内存。对象需要多大?JVM 根据类信息一算就知道——就是所有实例变量(不包括静态变量)占的空间。
分配方式取决于堆内存是否规整,而是否规整取决于垃圾收集器是否带压缩整理:
| 分配方式 | 适用场景 | 原理 |
|---|---|---|
| 指针碰撞(Bump the Pointer) | 堆内存规整(如 Serial、ParNew、CMS 带压缩) | 移动指针即可,高效 |
| 空闲列表(Free List) | 堆内存不规整(如 CMS 不压缩模式) | 维护一个"哪些空间空闲"的列表,分配时查找 |
指针碰撞: ┌────┬────┬────┬────┬──────────────────────┐ │ 对象│ 对象│ 对象│ 对象│ 空闲空间 │ └────┴────┴────┴────┴──────────────────────┘ ↑ 指针 分配后指针右移即可 空闲列表: ┌────┬ ┬────┬ ┬────┬──────────────┐ │ 对象│空闲│ 对象│ 空闲 │ 对象│ 空闲 │ └────┴ ┴────┴ ┴────┴──────────────┘ 需要查表找到合适大小的空闲块并发安全:两个人抢同一间房怎么办?
对象分配是高频操作,线程 A 和线程 B 可能同时抢同一块内存。JVM 用两种方案解决:
方案一:CAS + 失败重试
对分配动作做原子操作,抢到了就分配,抢不到就重试。
方案二:TLAB(Thread Local Allocation Buffer)
每个线程在 Eden 区预先分一小块私有空间,先在自己的地盘上分配,用完了再去公共区域用 CAS 抢。大部分情况下都在 TLAB 里分配,几乎无竞争。
// 开启 TLAB(默认开启)-XX:+UseTLAB面试加分点:TLAB 虽好,但空间有限。对象太大或 TLAB 用完,还是要走 CAS 在 Eden 区分配。
四、第三关:零值初始化——毛坯房刷白墙
内存分到后,JVM 会把这块空间全部初始化为零值(不包括对象头):
| 类型 | 零值 |
|---|---|
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0d |
| boolean | false |
| char | ‘\u0000’ |
| 引用类型 | null |
classPerson{intage;Stringname;booleanalive;}Personp=newPerson();// 此时:age = 0, name = null, alive = false// 但你一行赋值代码都还没执行!这一步保证了 Java 代码即使不赋初值也不会拿到随机垃圾值——C/C++ 程序员流下了羡慕的泪水。
注意:这里的零值初始化和构造方法中的赋值是两回事。如果构造方法里写了
age = 18,那先被初始化为 0,再被构造方法改为 18。
五、第四关:设置对象头——装门牌号和监控
这一步是很多人忽略的,但对 JVM 至关重要。JVM 会在对象头中设置:
对象头包含什么?
┌─────────────────────────────────────────────┐ │ 对象头 │ ├──────────────┬──────────────┬───────────────┤ │ Mark Word │ 类型指针 │ 数组长度 │ │ (8 字节) │ (4/8 字节) │ (仅数组对象) │ └──────────────┴──────────────┴───────────────┘Mark Word(标记字段)——存的是重量级信息:
- 对象的 HashCode(第一次调用时才计算并存入)
- GC 分代年龄(经过几次 GC 还活着)
- 锁状态标志(无锁、偏向锁、轻量级锁、重量级锁)
- 偏向线程 ID
类型指针——指向类元数据,JVM 通过它知道这个对象是Person还是Dog。
数组长度——只有数组对象才有,普通对象不需要。
面试常考点:synchronized 的锁升级过程,就记录在 Mark Word 中。从无锁 → 偏向锁 → 轻量级锁 → 重量级锁,Mark Word 的存储内容会随之变化。
六、第五关:执行构造方法——精装修
前面四步都是 JVM 自动完成的,到这一步终于轮到你的代码登场了。
JVM 会执行<init>()方法,也就是你写的构造方法:
classPerson{intage=18;// 实例变量赋值 → 编译后放进 <init>()Stringname="张三";// 实例变量赋值 → 编译后放进 <init>()Person(){// 构造方法age=25;// 会覆盖上面的 18System.out.println("对象创建完毕!");}}newPerson();// 执行顺序:// 1. 零值初始化(age=0, name=null)// 2. 实例变量赋值(age=18, name="张三")// 3. 构造方法体(age=25, 打印"对象创建完毕!")完整执行顺序
如果你有父类,顺序更完整:
1. 父类静态变量赋值 + 父类 static 代码块(类加载时,只执行一次) 2. 子类静态变量赋值 + 子类 static 代码块(类加载时,只执行一次) 3. 父类实例变量赋值 4. 父类构造方法 5. 子类实例变量赋值 6. 子类构造方法classAnimal{static{System.out.println("1: Animal 静态代码块");}{System.out.println("3: Animal 实例代码块");}Animal(){System.out.println("4: Animal 构造方法");}}classDogextendsAnimal{static{System.out.println("2: Dog 静态代码块");}{System.out.println("5: Dog 实例代码块");}Dog(){System.out.println("6: Dog 构造方法");}}newDog();// 输出:1 → 2 → 3 → 4 → 5 → 6为什么静态代码块只执行一次?因为类加载只发生一次。而实例代码块和构造方法是每次 new 都执行的。
七、回到全貌:一张图串起来
new Dog() │ ├─ ① 类加载检查 │ Animal 已加载?→ 是 │ Dog 已加载? → 否 → 触发类加载 │ ├─ 加载 Animal.class → 执行 Animal <clinit>(输出 1) │ └─ 加载 Dog.class → 执行 Dog <clinit>(输出 2) │ ├─ ② 分配内存(堆上分配 Dog 实例所需空间) │ ├─ ③ 零值初始化(所有字段设为默认值) │ ├─ ④ 设置对象头(Mark Word + 类型指针) │ └─ ⑤ 执行 <init>() ├─ 调用父类 <init>() │ ├─ Animal 实例代码块(输出 3) │ └─ Animal 构造方法(输出 4) ├─ Dog 实例代码块(输出 5) └─ Dog 构造方法(输出 6)八、面试速答模板
Q:Java 对象的创建过程?
A:五步——① 类加载检查:确保类已被加载,未加载则先触发类加载;② 分配内存:根据类信息计算大小,在堆上分配,方式有指针碰撞和空闲列表;③ 零值初始化:将内存空间初始化为零值,保证字段有默认值;④ 设置对象头:存入 HashCode、GC 分代年龄、锁状态、类型指针等;⑤ 执行构造方法:按先父类后子类的顺序执行实例变量赋值和构造方法体。
Q:对象分配内存时的并发问题怎么解决?
A:两种方案——① CAS + 失败重试,保证分配动作的原子性;② TLAB(线程本地分配缓存),每个线程在 Eden 区预分一小块私有空间,优先在私有空间分配,用完再用 CAS 在公共区域分配。TLAB 默认开启,能消除大部分竞争。
Q:对象头里存了什么?
A:主要存两块信息——Mark Word 存储 HashCode、GC 分代年龄、锁状态标志、偏向线程 ID 等;类型指针指向类元数据,JVM 据此确定对象属于哪个类。数组对象还会额外存储数组长度。
相关文章
原文阅读
内容有帮助?点赞、收藏、关注三连!评论区等你 💪