对象创建流程

候选人小周在面试美团 P6 时,面试官问道:

"一个对象是怎么创建出来的?new Object() 背后发生了什么?"

小周说:"先分配内存,再初始化..."面试官追问:"内存分配是怎么做的?指针碰撞和空闲列表有什么区别?"

小周答不上来。面试官继续追问:"TLAB 是什么?"

小周彻底卡住了...

一、核心问题:对象创建流程 🔴

1.1 问题拆解

第一层:完整流程(有哪些步骤?)
  "对象创建的完整步骤是什么?"
  考察点:类加载检查、内存分配、初始化

第二层:内存分配(怎么做?)
  "指针碰撞和空闲列表有什么区别?什么情况下用哪个?"
  考察点:GC 算法、内存规整

第三层:TLAB(怎么优化?)
  "TLAB 是什么?为什么需要 TLAB?"
  考察点:并发分配、CAS

1.2 ❌ 错误示范

候选人原话 A:"对象创建就是分配内存。"

问题诊断:对象创建包括类加载检查、内存分配、初始化、设置对象头等多个步骤。

候选人原话 B:"所有对象都在 Eden 区分配。"

问题诊断:大对象(超过 PretenureSizeThreshold)直接在老年代分配;TLAB 在 Eden 区为每个线程预分配。

1.3 标准回答

P5 级别:完整创建步骤

对象创建的完整流程

graph TD
    A[new Object()] --> B{类加载检查<br/>常量池中是否有类的符号引用}
    B -->|未加载| C[类加载验证准备解析初始化]
    B -->|已加载| D[分配内存]
    D --> E{GC 算法}
    E -->|标记-清除| F[空闲列表<br/>使用空闲列表记录可用空间]
    E -->|复制/标记-整理| G[指针碰撞<br/>使用指针将已用和空闲分开]
    F --> H[初始化为零值]
    G --> H
    H --> I[设置对象头]
    I --> J[执行构造函数<br/>\<init\>方法]

步骤详解

  1. 类加载检查:检查常量池中是否有该类的符号引用,检查该类是否已加载/初始化
  2. 分配内存:根据类元数据计算对象大小
  3. 初始化为零值:将分配到的内存初始化为零值(除对象头外)
  4. 设置对象头:设置 Mark Word(hashcode、GC 分代年龄、锁信息)和 Klass Pointer
  5. 执行构造函数:调用 <init> 方法

P6 级别:内存分配方法

指针碰撞(Bump the Pointer)

适用于带压缩整理的 GC 算法(如 Serial、ParNew、G1):

// 维护一个指针,指向已分配内存和空闲内存的边界
// 分配时:将指针向空闲方向移动对象大小距离

// Eden 区:
// [已分配空间 | 指针 | 空闲空间]
//                ↑ bump

// 优点:分配效率极高(只需移动一个指针)
// 缺点:需要 GC 时整理内存,保持连续空间

空闲列表(Free List)

适用于不带压缩整理的 GC 算法(如 CMS):

// 维护一个空闲块列表,记录每个可用内存块的大小和地址
// 分配时:查找合适的空闲块,更新列表

// 优点:无需整理内存
// 缺点:分配效率低(需要查找合适的块)

TLAB(Thread Local Allocation Buffer)

为减少并发分配时的竞争,JVM 为每个线程在 Eden 区预分配一块专属缓冲区:

// 开启 TLAB(默认开启)
-XX:+UseTLAB
-XX:TLABSize=...

// 每个线程有自己的 TLAB(通常 1~2MB)
// 在 TLAB 中分配对象时,只需移动 TLAB 指针
// TLAB 满了后,分配新的 TLAB(需要加锁)

// TLAB 优势:
// 1. 无需 CAS:TLAB 只被当前线程使用
// 2. 减少 Eden 区竞争:不同线程在不同的 TLAB 中分配

P7 级别:对象访问定位

对象的访问方式

对象创建后,需要通过 reference(引用)访问对象。JVM 有两种访问方式:

方式HotSpot 实现优点缺点
句柄访问reference 指向句柄池,句柄指向对象对象移动时只改句柄多一次跳转
直接指针访问reference 直接指向对象,对象头包含类型指针访问快(少一次跳转)对象移动时需更新所有 reference

HotSpot 使用直接指针访问

// reference 直接指向对象
Object obj = new Object();

// obj 引用在虚拟机栈的局部变量表中
// 对象在堆中
// 对象头中的 Klass Pointer 指向方法区的类元数据

【面试官心理】 这道题我能问到 P7 级别,是因为对象创建涉及了 GC 算法选择、并发分配优化、对象访问模型等多个维度。能说清 TLAB 原理和指针碰撞/空闲列表差异的候选人说明他理解了 JVM 的内存分配策略。

1.4 追问升级

追问:对象创建过程中,哪些步骤可能抛出异常?

  1. 类加载检查时:ClassNotFoundException(如果类未加载)
  2. 内存分配时:OutOfMemoryError: Java heap space(如果堆不够)
  3. 构造函数中:业务逻辑异常

二、生产避坑 🟡

2.1 大量小对象导致的 GC 压力

如果应用频繁创建大量短生命周期对象(如字符串拼接、集合批量操作),可能导致频繁 Minor GC。

解决:使用 StringBuilder、对象池、批量操作。

2.2 TLAB 的配置

# TLAB 配置
-XX:+UseTLAB          # 开启 TLAB(默认开启)
-XX:TLABSize=512k     # TLAB 大小
-XX:-ResizeTLAB       # 禁用 TLAB 自动调整
💡

面试加分点:能说出"JDK 17+ 的 Epsilon GC 不回收任何对象(无操作 GC),适合短生命周期程序和性能测试,可以精确测量对象分配开销",说明他关注了 JDK 新特性。

⚠️

面试陷阱:被问到"对象创建过程中,构造函数是什么时候被调用的",很多人会说"在分配内存之前"。准确答案是:在 new 指令分配内存后,在 <init> 方法执行时调用。字节码顺序是:new → dup → invokespecial <init>