← 返回题目列表

new 一个对象时,JVM 内部发生了什么?

高频 中等 第 11 / 34 题 更新于 2026/07/25
JVM对象创建类加载

简化版

new 一个对象大致五步:① 检查类是否已加载(没加载先触发类加载)→ ② 在堆上分配内存③ 内存零值初始化(字段先置默认值)→ ④ 设置对象头(类型指针、Mark Word 等)→ ⑤ 执行构造方法<init>,按你的代码赋真正的值)。之后引用变量指向这个对象。

详细版

User u = new User() 为例,JVM 干了这些事:

  1. 类加载检查:看 User 的类元信息在方法区里有没有。没有就先走类加载(加载→验证→准备→解析→初始化),确保类就绪。
  2. 分配内存:在上划一块 User 大小的内存。两种分配方式:
    • 指针碰撞:内存规整时,把「已用/未用」的分界指针往后挪对象大小即可;
    • 空闲列表:内存有碎片时,从一个记录空闲块的列表里找一块够大的。 用哪种取决于所用 GC 是否会整理内存(标记-整理/复制 → 规整 → 指针碰撞)。
  3. 零值初始化:把分配到的内存全部置零(默认值)。所以此时字段 int 是 0、引用是 null——这也是为什么实例字段不赋初值也能用
  4. 设置对象头:往对象头里写入类型指针(指向类元信息)、Mark Word(哈希码、GC 分代年龄、锁标志等)。
  5. 执行 <init>(构造方法):这才轮到你写的构造逻辑和字段显式赋值真正执行,对象变成「你期望的样子」。

完整版教学

一、两次「初始化」别搞混

面试常追问:对象创建过程里有两次赋值,区别是什么?

  • 第 3 步「零值初始化」:JVM 把内存清零,字段得到默认值(0、false、null)。这一步保证「对象字段即使不显式赋值也有确定初值」。
  • 第 5 步「构造方法执行」:执行 <init>,包括字段的显式初始化int age = 18)和构造器里的代码,字段得到你写的值

所以 int age = 18 这个字段,实际经历「先被置 0 → 再被赋 18」两步。理解这个能解释很多「半初始化对象」的并发问题(见下)。

二、并发下的坑:对象「半初始化」

第 2~5 步不是原子的,且步骤 2(分配内存)和 5(初始化)之间可能被指令重排。这正是双重检查锁单例(DCL)必须给字段加 volatile 的原因:

instance = new Singleton();
// 实际拆成:① 分配内存 → ② 执行构造初始化 → ③ 引用指向内存
// 可能被重排成 ①③②:引用已非 null,但对象还没初始化完

如果重排成 ①③②,另一个线程可能拿到「引用不为 null 但对象还没构造完」的半成品对象volatile 通过内存屏障禁止这个重排(详见「volatile 的作用和原理」那道题)。

三、从语义对象到优化结果——两个关键机制

Java 语义上执行了对象创建,但 JIT 在证明对象不逃逸且行为等价时,可能消除实际对象分配。需要区分语言语义与优化后的机器执行:

  • 逃逸分析 + 标量替换:如果 JIT 证明对象不会逃逸,可能把字段拆成若干标量并消除完整对象分配。最终值可能进入寄存器或栈槽,不能简单概括为创建了一个完整的“栈上对象”。
  • TLAB(线程本地分配缓冲):堆是共享的,多线程同时分配内存要同步,很慢。于是每个线程在 Eden 里预先分一小块私有区域(TLAB),线程在自己的 TLAB 里分配对象无需加锁,快得多。TLAB 用完了再向堆申请。

记忆点:常见快路径是在 Eden 的 TLAB 中做指针碰撞;不逃逸对象可能被标量替换,从而根本不形成完整堆对象。

四、对象在内存里长什么样

常见 HotSpot 堆对象由对象头、实例数据、对齐填充三部分组成。对象头保存类型元数据指针及 Mark Word 等运行时状态,实例数据承载字段,对齐填充则让总大小满足当前 JVM 的对象对齐粒度。对象对齐常见为 8 字节,但它属于可配置的实现细节,不是 Java 规范固定值。构造器执行前 JVM 已建立可识别的对象头;具体字节数必须结合位数、压缩类指针和 JDK 版本判断。

五、分配快路径、慢路径与可见性

线程能在 TLAB 中分配时,常见动作只是移动私有的顶部指针:

new_top = tlab_top + object_size
若 new_top <= tlab_end:分配成功
否则:申请新 TLAB 或进入共享堆慢路径

假设 TLAB 剩余 256 字节,新对象按对齐后需要 64 字节,则本次快路径只需更新指针;若对象需要 320 字节,就无法放入当前 TLAB。它可能申请新 TLAB、直接在共享区域分配,或触发 GC,具体由 JVM 决定。

对象引用不能在构造完成前不安全地发布给其他线程。构造期间执行 registry.add(this) 就可能让别的线程观察到尚未完成初始化的字段。正确发布可使用锁、volatile 引用、静态初始化或并发容器提供的 happens-before 关系。

对象大小还会改变路径:当前 TLAB 只剩 96 字节时,按对齐后 64 字节的对象仍能走快路径,而 128 字节对象必须申请新 TLAB 或进入慢路径。这里的数字只用于展示判定方法,真实 TLAB 大小与浪费阈值由 HotSpot 自适应策略和参数决定。分析分配性能时还要区分首次解释执行与 JIT 预热后的稳态,否则逃逸分析尚未生效会造成错误结论。

对象对齐后大小 ≤ TLAB 剩余空间 → TLAB 快路径
对象对齐后大小 > TLAB 剩余空间 → 新 TLAB 或共享堆慢路径

六、对象“在哪里”要区分语义与优化

层次应该怎样描述
Java 语义new 创建具有类身份和字段状态的对象
解释器/未优化执行常见在 Java 堆分配对象存储
JIT 优化后不逃逸对象可能被标量替换,完整分配可被消除
运行时分配路径常见优先使用 TLAB,失败再走慢路径

逃逸分析本身不是“把对象搬到栈上”的同义词。HotSpot 常见收益是标量替换和锁消除;最终机器码可能只保留几个局部标量,甚至把整个对象计算消除。

面试中说“对象都在堆上”过于绝对,说“逃逸分析后一定栈上分配”同样不准确;应说明语义、常见实现和 JIT 优化三个层次。

对象创建性能分析还应区分解释执行、C1/C2 编译阶段以及预热后的稳态结果。

七、常见误区与追问

  • 误区:执行构造方法就是对象创建的第一步。 类检查、内存分配、默认值处理和对象头设置通常发生在 <init> 之前。
  • 误区:逃逸分析会把所有不逃逸对象完整地放到线程栈。 常见优化是标量替换或消除分配,是否实施取决于 JIT 证明和成本。
  • 误区:有 TLAB 就完全不存在分配竞争。 TLAB 快路径减少竞争,申请新 TLAB 和共享区域分配仍需协调。
  • 追问:为什么实例字段有默认值而局部变量没有? 对象存储在构造代码前具有零值语义;局部变量则必须通过编译器确定赋值检查。
  • 追问:大对象也一定进入 TLAB 吗? 不一定;超过当前 TLAB 或相关阈值时通常走共享分配路径,具体取决于 JVM。
  • 追问:对象头什么时候写入? 分配后、构造器执行前,JVM 要建立类型元数据指针和初始 Mark Word 等状态。
  • 追问:如何验证分配是否被消除? 可结合 JIT 编译日志、诊断参数或 JMH 与合适分析工具,不能仅凭源码判断。

八、加强记忆

记忆主线是“类检查、分配、默认值、对象头、构造器”,但要把它当作常见 HotSpot 语义路径而非每次都原样执行的机器步骤。常规小对象优先在 Eden 的 TLAB 中做指针碰撞,放不下时再申请缓冲区或进入共享堆慢路径。JIT 若证明对象不逃逸,可能通过标量替换消除完整分配,这不等于固定创建一个栈上对象。最后用“构造中不泄露 this、发布时建立 happens-before”记住并发可见性的边界。