【JVM原理详解】17-对象的创建全过程

发布时间:2026/7/26 23:42:05
【JVM原理详解】17-对象的创建全过程 对象的创建全过程引言在 Java 中我们每天都在用new关键字创建对象一句Object obj new Object()背后JVM 究竟做了哪些工作对象的创建并非简单地分配一块内存然后填上数据它涉及类加载、内存分配、并发安全、内存初始化、对象头设置以及构造方法执行等多个环节。理解对象的创建全过程是深入 JVM 内存模型的起点也是理解后续 GC、锁优化、内存对齐等专题的基础。本篇将以 HotSpot JVM 为例拆解对象从new指令到可用对象的完整流程。一、对象创建的五步流程总览当 JVM 遇到new字节码指令时大致会经过以下五个步骤------------------------------------------------------------------ | new 指令执行流程 | ------------------------------------------------------------------ | | | 1. 类加载检查 | | 常量池中能否找到类的符号引用 → 是否已加载/解析/初始化 | | | | | | 未加载 | | v | | 执行类加载过程 | | | | 2. 分配内存 | | 在 Java 堆中划分出确定大小的内存块 | | -- 指针碰撞 (Serial / ParNew带压缩) | | -- 空闲列表 (CMS不带压缩) | | -- TLAB 本地线程分配缓冲 (提升并发效率) | | | | 3. 初始化零值不含对象头 | | 内存分配后将分配空间初始化为零值 | | | | 4. 设置对象头 | | Mark Word (哈希码/GC分代年龄/锁状态) | | Klass Pointer (类型指针指向类元数据) | | (数组对象还有数组长度) | | | | 5. 执行 init 构造方法 | | 即 Java 代码层面的构造函数 | | 此时对象才真正可用 | | | ------------------------------------------------------------------下面逐步深入每个环节。二、第一步类加载检查JVM 遇到new指令时首先会去检查这个指令的参数能否在常量池中定位到一个类的符号引用并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有则必须先执行相应的类加载过程。类加载过程包括加载 → 验证 → 准备 → 解析 → 初始化。这一步确保了在分配内存之前JVM 已经知道了要创建的对象属于哪个类、有多大、有哪些字段。// 第一次使用 User 类触发类加载publicclassObjectCreationDemo{publicstaticvoidmain(String[]args){// new 指令 → 检查 User 是否已加载// 若未加载则先加载 User.classUserusernewUser(Alice,25);}}classUser{privateStringname;privateintage;publicUser(Stringname,intage){this.namename;this.ageage;}}为什么要在分配内存前先做类加载因为对象所需内存大小在类加载完成后才能确定。JVM 需要根据类的字段布局计算出对象的大小才能在堆中划分出精确的内存块。类加载阶段完成的元信息字段元数据、方法元数据是后续内存分配的依据。三、第二步分配内存类加载完成后JVM 就可以为新对象分配内存了。对象所需内存的大小在类加载完成后即可完全确定分配的过程就是把一块确定大小的内存从 Java 堆中划分出来。3.1 两种分配方式根据 Java 堆是否规整即内存是否连续规整、无碎片有两种分配方式指针碰撞Bump the Pointer假设 Java 堆中内存是绝对规整的所有用过的内存放一边空闲的内存放另一边中间放着一个指针作为分界点的指示器。分配内存就是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。---------------------------------------------------------- | 指针碰撞 (Bump the Pointer) | ---------------------------------------------------------- | | | 已分配内存 | 空闲空间 | | [obj1][obj2][obj3] | ................................. | | ^ | | | | | 分配指针 | | | | 分配 obj4指针右移 obj4 大小 | | [obj1][obj2][obj3][obj4] | ........................... | | ^ | | | | | 新指针 | ----------------------------------------------------------空闲列表Free List如果 Java 堆中的内存不是规整的已使用的内存和空闲内存相互交错JVM 就需要维护一个列表记录哪些内存块是可用的在分配时从列表中找到一块足够大的空间划分给对象实例并更新列表上的记录。---------------------------------------------------------- | 空闲列表 (Free List) | ---------------------------------------------------------- | | | 堆内存 | | [obj1] ... [obj2] [free 16B] [obj3] [free 32B] [obj4] | | | | 空闲列表 | | ---------------- | | | addr | size | | | ---------------- | | | 0x1008 | 16B | | | | 0x1030 | 32B | | | | 0x10A0 | 64B | | | ---------------- | | | | 分配时遍历列表找到 size 对象大小 的块 | ----------------------------------------------------------选择哪种分配方式由 Java 堆是否规整决定而 Java 堆是否规整又由所采用的 GC 收集器是否带有空间压缩整理能力决定GC 收集器堆是否规整分配方式Serial / ParNew (带压缩)规整指针碰撞CMS (不带压缩)不规整空闲列表G1 (Region 内规整)Region 内规整TLAB 指针碰撞ZGC / Shenandoah不规整空闲列表3.2 并发安全问题CAS TLAB对象创建在虚拟机中非常频繁即使仅仅修改一个指针所指向的位置在并发情况下也不是线程安全的。可能出现正在给对象 A 分配内存指针还没来得及修改对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案方案一CAS 失败重试。对分配动作进行原子操作保证更新指针时的线程安全。CASCompare-And-Swap是一种无锁机制它比较内存中的值与预期值若相同则更新为新值否则重试。HotSpot 在分配内存时使用 CAS 保证指针更新的原子性。方案二TLABThread Local Allocation Buffer。把内存分配的动作按照线程划分在不同的空间之中进行即每个线程在 Java 堆中预先分配一小块私有内存。哪个线程要分配内存就在哪个线程的 TLAB 上分配只有 TLAB 用完了并需要分配新的 TLAB 时才需要 CAS。------------------------------------------------------------------ | TLAB 内存分配 | ------------------------------------------------------------------ | | | Java 堆 | | ----------------------------------------------------------- | | | Thread-1 的 TLAB | Thread-2 的 TLAB | 其他堆空间 | | | | [obj][obj][obj] | [obj][obj] | ............... | | | | ^空闲指针 | ^空闲指针 | | | | ----------------------------------------------------------- | | | | 线程在自己的 TLAB 内分配无竞争极快 | | TLAB 耗尽后CAS 申请新的 TLAB | | | ------------------------------------------------------------------TLAB 是 HotSpot 提升对象分配效率的核心机制。相关参数# 开启 TLAB默认开启-XX:UseTLAB# 设置 TLAB 占单个 Eden 区的比例默认 1%-XX:TLABWasteTargetPercent1# 查看 TLAB 相关信息-XX:PrintTLAB为什么 TLAB 能大幅提升性能绝大多数对象在 TLAB 中分配线程无需竞争全局指针分配动作退化为指针碰撞效率接近栈上分配。只有大对象无法放入 TLAB或 TLAB 耗尽时才需要全局 CAS。3.3 大对象的分配对于大型对象如长数组、大字符串JVM 会直接在堆中分配而不经过 TLAB。因为大对象如果放入 TLAB 会很快耗尽缓冲区导致频繁的 TLAB 重分配。# 设置大对象阈值G1 中超过 Region 一半的对象被视为 Humongous 对象# G1 默认 Region 大小为堆的 1/2048最小 1MB最大 32MB-XX:G1HeapRegionSize16m四、第三步初始化零值内存分配完成后JVM 会将分配到的内存空间不包括对象头都初始化为零值。这一步操作保证了对象的实例字段在 Java 代码中可以不赋初值就能直接使用。publicclassZeroValueDemo{// 以下字段在 init 执行前已被零值初始化privateintcount;// 0privatebooleanflag;// falseprivatelongtimestamp;// 0Lprivatedoublescore;// 0.0privateObjectref;// nullprivatecharch;// \u0000publicvoidprint(){// 不赋初值也能正常使用System.out.println(count);// 0System.out.println(flag);// falseSystem.out.println(ref);// null}}各类型零值表数据类型零值int0long0Lshort(short)0char‘\u0000’byte(byte)0booleanfalsefloat0.0fdouble0.0d引用类型null注意如果使用了-XX:ZeroTLAB参数默认关闭TLAB 分配的内存不会被零值初始化而是由后续步骤负责。但默认情况下零值初始化在 TLAB 分配时就会完成。五、第四步设置对象头零值初始化完成后JVM 接下来要对对象进行必要的设置将这些信息存放在对象头Object Header中。对象头主要包括Mark Word存储对象自身的运行时数据如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID 等。Klass Pointer类型指针指向对象的类元数据Class 元数据JVM 通过这个指针确定对象是哪个类的实例。数组长度仅数组对象有如果是数组对象对象头中还会有一块用于记录数组长度的数据。---------------------------------------------------------- | 对象头 (Object Header) 结构 | ---------------------------------------------------------- | | | --------------------------------------- | | | Mark Word | 32bit / 64bit | | | --------------------------------------- | | | Klass Pointer | 32bit / 64bit | 压缩后 4B | | --------------------------------------- | | | array length | 32bit (仅数组) | | | --------------------------------------- | | | | 普通对象头大小 | | 64位 JVM 开启指针压缩 8B (Mark Word) 4B 12B | | 64位 JVM 未开启压缩 8B (Mark Word) 8B 16B | | 32位 JVM 4B (Mark Word) 4B 8B | ----------------------------------------------------------Mark Word 在不同锁状态下会复用存储空间这是后续锁升级专题的核心内容。本篇只需理解对象头在这个阶段被设置其中 Klass Pointer 指向刚刚加载的类元数据Mark Word 中的哈希码、GC 分代年龄等会被设为初始值哈希码延迟到第一次调用hashCode()时才计算并写入。相关 JVM 参数# 开启指针压缩64位 JVM 默认开启堆 32GB 时有效-XX:UseCompressedOops# 关闭指针压缩-XX:-UseCompressedOops六、第五步执行 构造方法前面四步完成后从 JVM 的角度看一个新的对象已经产生了但从 Java 程序的视角看对象的创建才刚刚开始——构造方法还没有执行。init方法是 JVM 层面对应 Java 构造函数的方法。在这个阶段JVM 会执行调用父类构造方法super()对实例字段赋初始值显式赋值执行构造方法体中的代码publicclassInitOrderDemo{privateinta10;// 显式赋值在 init 中执行privateintbinitB();// 方法调用赋值privatestaticintc20;// 类初始化阶段执行不在 initpublicInitOrderDemo(){// 3. 构造方法体System.out.println(构造方法: aa, bb);}privateintinitB(){// 2. 显式赋值时调用returna*2;}publicstaticvoidmain(String[]args){// 完整执行顺序// 1. 类加载检查InitOrderDemo 已加载// 2. 分配内存TLAB// 3. 零值初始化a0, b0// 4. 设置对象头// 5. init:// a) super() 隐式调用// b) a 10// c) b initB() 20// d) 打印 构造方法: a10, b20InitOrderDemodemonewInitOrderDemo();}}init方法执行完毕后一个真正可用的 Java 对象才算创建完成。七、完整流程代码示例下面用一个完整的例子串联整个流程并使用 JOLJava Object Layout工具观察对象结构// 需引入依赖org.openjdk.jol:jol-core:0.17importorg.openjdk.jol.info.ClassLayout;publicclassObjectCreationFullDemo{publicstaticvoidmain(String[]args){// new 指令触发完整创建流程PersonpersonnewPerson(Bob,30);// 查看对象内存布局System.out.println(ClassLayout.parseInstance(person).toPrintable());}}classPerson{privateStringname;// 引用类型4B压缩指针privateintage;// 4BpublicPerson(Stringname,intage){this.namename;this.ageage;}}输出示例64位 JVM开启指针压缩# OFFSET SIZE TYPE DESCRIPTION VALUE# 0 4 (object header: Mark) 0x00000001# 4 4 (object header: Mark) 0x00000000# 8 4 (object header: Klass) 0x2000c005# 12 4 int Person.age 30# 16 4 ref Person.name Bob# 20 4 (alignment padding)# Instance size: 24 bytes可以看到Mark Word 占 8 字节OFFSET 0-7Klass Pointer 占 4 字节压缩后实例数据 8 字节对齐填充 4 字节24 字节是 8 的整数倍。实践要点TLAB 是性能关键生产环境务必确保-XX:UseTLAB开启默认开启。高并发场景下若关闭 TLAB所有线程竞争全局指针分配性能会急剧下降。大对象分配需谨慎避免在循环中创建大数组或大字符串大对象无法走 TLAB 快速路径且可能直接进入老年代增加 GC 压力。可通过-XX:PretenureSizeThreshold仅 Serial/ParNew控制大对象直接进入老年代的阈值。对象头是内存开销每个对象都有对象头对于小对象如只含一个 int 字段的对象对象头占比超过 60%。在设计缓存、集合时需考虑对象头的内存开销适当使用基本类型数组替代包装类型。指针压缩的边界-XX:UseCompressedOops在堆小于 32GB 时有效超过 32GB 会自动关闭此时对象头从 12 字节膨胀到 16 字节内存开销显著增加。这也是为什么 32GB 常被视为堆大小的一个性能拐点。构造方法中的陷阱init执行期间对象处于半初始化状态若在构造方法中调用可被重写的方法虚方法或启动新线程传递this可能导致子类读到未初始化完成的字段值。这是常见的构造方法泄漏 this问题。小结对象创建经历五步类加载检查 → 分配内存 → 初始化零值 → 设置对象头 → 执行init前三步在 JVM 层面完成后两步连接到 Java 代码层面。内存分配方式由堆是否规整决定指针碰撞要求规整堆空闲列表适应碎片堆TLAB通过线程私有缓冲区大幅减少竞争是 HotSpot 的默认优化手段。并发安全通过CAS TLAB组合保障TLAB 内无锁分配TLAB 耗尽时 CAS 申请补充。对象头在第四步设置包含Mark Word和Klass Pointer是后续锁、GC、类型判断的基础。init构造方法执行后对象才真正可用理解init的执行顺序super → 字段赋值 → 构造体有助于排查字段初始化问题。下一篇我们将深入对象头的内部结构用 JOL 工具实测对象在不同状态下的内存布局。更多内容JVM调优实战

相关新闻