跳至正文
来两杯美式
返回

一文拿下 JVM(终章):对象创建的 7 个步骤

By 来两杯美式
发布于更新于

整个「一文拿下 JVM」系列走到终章。回到最基础的问题——一个 new Object() 在 JVM 内部到底经历了什么? 看似一行代码,JVM 内部走了 7 步。

一、整体流程

对象创建 7 步

一个对象从「代码里写 new」到「可以使用」大致走这 7 步:

  1. 类加载检查:检查这个指令参数是否能在常量池定位到一个类的符号引用,并检查符号引用代表的类是否已被加载、解析和初始化。如果没有,执行相应的类加载
  2. 分配内存:虚拟机为新生对象分配空间(对象所需内存大小在类加载后即可确认)
  3. 划分方式:使用指针碰撞 或 空闲列表
  4. 线程安全保证:通过 CAS + 失败重试,或 TLAB 解决
  5. 内存零值初始化:将分配的内存空间初始化为零值(不包括对象头)
  6. 对象头设置:设置对象哈希码、GC 分代年龄、类元数据指针等
  7. 执行 <init> 方法:执行构造方法

下面逐个拆开讲。

二、类加载检查

当 JVM 遇到一条 new 指令时:

  1. 检查这个指令参数是否能在常量池中定位到一个类的符号引用
  2. 检查符号引用代表的类是否已被加载、解析和初始化
  3. 如果没有,执行相应的类加载流程

类加载的详细过程(Loading → Linking → Initializing)是另一个大话题,这里不展开。

三、分配内存

对象所需内存大小在类加载完成后即可确认(因为对象的字段类型和数量在 class 文件里就定下来了)。

分配空间 = 从堆中划分一块空间

3.1 划分方式

堆是否「规整」决定了采用哪种分配方式。

指针碰撞示意

方式适用场景原理
指针碰撞(Bump the Pointer)Java 堆规整(用过的内存放一边、空闲内存放另一边)中间放指针作为分界点指示器,向空闲空间挪指针,距离为新分配对象大小的距离
空闲列表(Free List)Java 堆不规整虚拟机维护一个列表,记录可用的内存块,从列表找出一块足够大的空间划分给对象实例,并更新列表记录

Java 堆是否规整由采用的垃圾收集器是否带压缩整理功能决定

  • 使用 Serial / ParNew 等带 Compact 过程的收集器 → 指针碰撞
  • CMS 这种基于 Mark-Sweep 算法的收集器 → 空闲列表

空闲列表示意

3.2 划分空间的线程安全性

多线程并发分配时,必须保证分配动作的原子性。两种方案:

方案 1:CAS + 失败重试

分配空间动作同步处理——CAS 配上失败重试。这是JVM 默认的方案

// 伪代码
do {
    old = pointer;
    new = old + size;
} while (!compareAndSwap(pointer, old, new));

方案 2:TLAB(Thread Local Allocation Buffer)

把内存分配动作划分在不同空间中——即为每个线程在堆中预先分配一小块内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer,TLAB)。

TLAB 内存布局示意(文字版):堆的 Eden 区被切分成多块连续的小区域,每块归属一个线程专属使用(避免竞争)。当某线程的 TLAB 用完,需要申请新的 TLAB 时才需要同步锁定。没有锁定开销——这是 TLAB 能提升分配性能的核心。

2017 年冷知识:当年 HotSpot 默认开启 TLAB,且对 Eden 区再按线程分配 TLAB。每个线程在自己的 TLAB 里分配,不需要加锁——几乎无开销。

四、内存零值初始化

内存分配完后,JVM 将分配的内存空间初始化为零值(不包括对象头)。

public class User {
    private int age;       // 自动初始化为 0
    private String name;   // 自动初始化为 null
    private boolean flag;  // 自动初始化为 false
}

这保证了对象实例字段在 Java 代码中可以不赋初始值就可使用。比如你只 new User(),不写任何字段,访问 age 得到 0 而非编译错误。

五、对象头设置

接下来虚拟机对对象进行必要的设置,把以下信息存放在对象头里:

对象头在 64 位 JVM 上通常占 12 字节(开启压缩指针时)或 16 字节(未开启)。这一块在 synchronized 锁优化里非常关键——锁状态信息就存在对象头里。

Mark Word 布局示意(文字版):64 位 JVM 下,Mark Word 共 8 字节,根据锁状态会复用存储不同信息——无锁时存哈希码+分代年龄,偏向锁存线程 ID+epoch,轻量级锁存栈中锁记录指针,重量级锁存 monitor 指针。Mark Word 设计是节省空间的典范——同一个 8 字节空间被多种用途复用。

Mark Word 的结构会随着锁状态变化:

  • 无锁:哈希码 + 分代年龄
  • 偏向锁:线程 ID + epoch + 分代年龄
  • 轻量级锁:指向栈中锁记录的指针
  • 重量级锁:指向 monitor 的指针

六、执行 <init> 方法

最后一步:执行 Java 字节码中的 <init> 方法——也就是构造方法

按代码里的赋值语句、构造器调用顺序依次执行:

public class User {
    private String name = "init";  // 1. 字段默认值(已经在内存零值初始化时设置)
                                   // 2. 字段显式赋值(<init> 中执行)

    {
        System.out.println("init block");  // 3. 初始化块
    }

    public User() {
        this.name = "constructor";  // 4. 构造方法体
    }
}

执行顺序:父类 <init> → 字段默认值 → 字段显式赋值 → 初始化块 → 构造方法体 → 子类 <init>

七、对象内存布局总结

以 64 位 JVM、开启压缩指针为例,Java 对象在堆内存中的布局:

┌──────────────────────┐
│   Mark Word (8 字节)  │  ← 哈希码 / GC 年龄 / 锁状态
├──────────────────────┤
│   Class Pointer (4 字节)│  ← 指向类元数据(开启压缩)
├──────────────────────┤
│   Instance Data       │  ← 字段实际值(int 4 字节、long 8 字节……)
├──────────────────────┤
│   Padding (对齐填充)   │  ← 保证对象大小是 8 字节的倍数
└──────────────────────┘

总大小 = 12 + 字段大小总和 + padding(向上对齐到 8 的倍数)。 这就是为什么 Java 对象至少占 16 字节——一个空 Object 在 64 位 JVM 上是 16 字节。

八、实战启示

8.1 不要在循环里 new 对象

// 反例
for (int i = 0; i < 1000000; i++) {
    User u = new User(i, "name" + i);  // 100 万次分配 + GC
}

// 正例
User u = new User();
for (int i = 0; i < 1000000; i++) {
    u.setId(i);
    u.setName("name" + i);
    // 业务逻辑
}

当然,现代 JVM 配合逃逸分析 + 标量替换会把 u 拆成局部变量,循环里其实没有对象分配。但保持好的习惯没坏处。

8.2 大对象要小心

大对象(一般指需要大量连续内存空间的 Java 对象,如很长的字符串、很大的数组)直接进入 Old 区

这意味着:

应对方案

8.3 对象头是 synchronized 的关键

如果你在写自定义锁无锁并发框架,要记住:偏向锁、轻量级锁的状态都存在对象头里。HashCode 冲突对象头竞争激烈都会影响锁升级路径。

当年(2017)JEP 提案里禁用偏向锁的讨论已经出现——理由是偏向锁在多线程竞争场景下撤销成本高。在 Java 15 之后,默认禁用偏向锁了(-XX:-UseBiasedLocking)。

九、整个系列回顾

「一文拿下 JVM」系列到这一篇就结束了,4 篇覆盖了 JVM 的核心:

主题重点
内存区域全景堆/栈/PC/方法区
参数配置与常用命令jps/jinfo/jstat/jmap/jstack
逃逸分析与代码优化同步消除/标量替换/栈上分配
番外OOM 排查与 MAT / JVisualVMdump 分析、GC Roots 追引用
终章对象创建的 7 个步骤类加载 → 内存分配 → init

这套知识到 2026 年依然有效——JVM 的核心设计几十年来没大变。变的只是 GC 算法(G1 → ZGC → Shenandoah)和一些默认参数。新人读这套笔记,再去看 2020 年后的 ZGC 调优指南,会发现基础一点没变。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
JVM
第 5 / 6 篇
查看系列全部文章
  1. 01.一文拿下 JVM(上):内存区域全景
  2. 02.一文拿下 JVM(中):参数配置与常用命令
  3. 03.一文拿下 JVM(下):逃逸分析与代码优化
  4. 04.一文拿下 JVM(番外):OOM 排查与 MAT / JVisualVM 实战
  5. 05.一文拿下 JVM(终章):对象创建的 7 个步骤
  6. 06.一文拿下 JVM(锁篇):synchronized 锁的升级与三种锁对比

上一篇
一文拿下 JVM(锁篇):synchronized 锁的升级与三种锁对比
下一篇
一文拿下 JVM(番外):OOM 排查与 MAT / JVisualVM 实战