基础篇五 你以为 new 一下就完事了?Java 对象诞生背后的五道关卡

张开发
2026/4/21 2:17:11 15 分钟阅读

分享文章

基础篇五 你以为 new 一下就完事了?Java 对象诞生背后的五道关卡
文章目录一、全流程概览二、第一关类加载检查——JVM 认不认识你三、第二关分配内存——给对象找个房子并发安全两个人抢同一间房怎么办四、第三关零值初始化——毛坯房刷白墙五、第四关设置对象头——装门牌号和监控对象头包含什么六、第五关执行构造方法——精装修完整执行顺序七、回到全貌一张图串起来八、面试速答模板个人网站写代码时new Person()一敲对象就出来了——就像你在餐厅点个菜菜就端上来了。但你有没有想过厨房里到底经历了什么食材采购、清洗切配、大火爆炒、摆盘装饰……你看到的只是最后一秒。Java 对象的创建也是一样。你以为只是一句newJVM 却在背后默默跑了五道流程哪一道出了问题你的对象都活不了。今天我们就去后厨看看一个 Java 对象到底是怎么被造出来的。一、全流程概览一个对象从new到可用要经过以下五步new Person() │ ├─ ① 类加载检查 → 你这个类 JVM 认识吗 ├─ ② 分配内存 → 给对象找个房子 ├─ ③ 零值初始化 → 毛坯房刷白墙字段设默认值 ├─ ④ 设置对象头 → 装门牌号和监控HashCode、GC 分代年龄等 └─ ⑤ 执行构造方法 → 精装修你写的赋值逻辑下面逐步拆解保证你看完就懂。二、第一关类加载检查——JVM 认不认识你当 JVM 遇到new Person()第一件事不是分配内存而是问Person 这个类加载了吗就像你去酒店入住前台先查你有没有预约记录。没有对不起先去办手续。PersonpnewPerson();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 失败重试对分配动作做原子操作抢到了就分配抢不到就重试。方案二TLABThread Local Allocation Buffer每个线程在 Eden 区预先分一小块私有空间先在自己的地盘上分配用完了再去公共区域用 CAS 抢。大部分情况下都在 TLAB 里分配几乎无竞争。// 开启 TLAB默认开启-XX:UseTLAB面试加分点TLAB 虽好但空间有限。对象太大或 TLAB 用完还是要走 CAS 在 Eden 区分配。四、第三关零值初始化——毛坯房刷白墙内存分到后JVM 会把这块空间全部初始化为零值不包括对象头类型零值int0long0Lfloat0.0fdouble0.0dbooleanfalsechar‘\u0000’引用类型nullclassPerson{intage;Stringname;booleanalive;}PersonpnewPerson();// 此时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{intage18;// 实例变量赋值 → 编译后放进 init()Stringname张三;// 实例变量赋值 → 编译后放进 init()Person(){// 构造方法age25;// 会覆盖上面的 18System.out.println(对象创建完毕);}}newPerson();// 执行顺序// 1. 零值初始化age0, namenull// 2. 实例变量赋值age18, name张三// 3. 构造方法体age25, 打印对象创建完毕完整执行顺序如果你有父类顺序更完整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八、面试速答模板QJava 对象的创建过程A五步——① 类加载检查确保类已被加载未加载则先触发类加载② 分配内存根据类信息计算大小在堆上分配方式有指针碰撞和空闲列表③ 零值初始化将内存空间初始化为零值保证字段有默认值④ 设置对象头存入 HashCode、GC 分代年龄、锁状态、类型指针等⑤ 执行构造方法按先父类后子类的顺序执行实例变量赋值和构造方法体。Q对象分配内存时的并发问题怎么解决A两种方案——① CAS 失败重试保证分配动作的原子性② TLAB线程本地分配缓存每个线程在 Eden 区预分一小块私有空间优先在私有空间分配用完再用 CAS 在公共区域分配。TLAB 默认开启能消除大部分竞争。Q对象头里存了什么A主要存两块信息——Mark Word 存储 HashCode、GC 分代年龄、锁状态标志、偏向线程 ID 等类型指针指向类元数据JVM 据此确定对象属于哪个类。数组对象还会额外存储数组长度。相关文章原文阅读内容有帮助点赞、收藏、关注三连评论区等你

更多文章