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

一个对象从「代码里写 new」到「可以使用」大致走这 7 步:
- 类加载检查:检查这个指令参数是否能在常量池定位到一个类的符号引用,并检查符号引用代表的类是否已被加载、解析和初始化。如果没有,执行相应的类加载
- 分配内存:虚拟机为新生对象分配空间(对象所需内存大小在类加载后即可确认)
- 划分方式:使用指针碰撞 或 空闲列表
- 线程安全保证:通过 CAS + 失败重试,或 TLAB 解决
- 内存零值初始化:将分配的内存空间初始化为零值(不包括对象头)
- 对象头设置:设置对象哈希码、GC 分代年龄、类元数据指针等
- 执行
<init>方法:执行构造方法
下面逐个拆开讲。
二、类加载检查
当 JVM 遇到一条 new 指令时:
- 检查这个指令参数是否能在常量池中定位到一个类的符号引用
- 检查符号引用代表的类是否已被加载、解析和初始化
- 如果没有,执行相应的类加载流程
类加载的详细过程(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 能提升分配性能的核心。
- TLAB 用完并分配新的 TLAB 时需要同步锁定
- TLAB 是否开启控制参数:
-XX:+/-UseTLAB
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 而非编译错误。
五、对象头设置
接下来虚拟机对对象进行必要的设置,把以下信息存放在对象头里:
- 对象哈希码(Object HashCode)
- 对象的 GC 分代年龄(决定什么时候进入 Old 区)
- 类的元数据指针(指向方法区的 Class 对象)
对象头在 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 区。
这意味着:
- 大对象绕过了 Young GC——直接占用老年代空间
- 大对象容易触发提前的 Full GC
- 大对象分配时容易触发空间分配担保——可能因为连续空间不足直接 OOM
应对方案:
- 避免单个对象过大(如单个 List 超过百万级)
- 拆分大对象为多个小对象
- 调高
-XX:PretenureSizeThreshold(默认值 0 即不开启)
8.3 对象头是 synchronized 的关键
如果你在写自定义锁或无锁并发框架,要记住:偏向锁、轻量级锁的状态都存在对象头里。HashCode 冲突或对象头竞争激烈都会影响锁升级路径。
当年(2017)JEP 提案里禁用偏向锁的讨论已经出现——理由是偏向锁在多线程竞争场景下撤销成本高。在 Java 15 之后,默认禁用偏向锁了(
-XX:-UseBiasedLocking)。
九、整个系列回顾
「一文拿下 JVM」系列到这一篇就结束了,4 篇覆盖了 JVM 的核心:
| 篇 | 主题 | 重点 |
|---|---|---|
| 上 | 内存区域全景 | 堆/栈/PC/方法区 |
| 中 | 参数配置与常用命令 | jps/jinfo/jstat/jmap/jstack |
| 下 | 逃逸分析与代码优化 | 同步消除/标量替换/栈上分配 |
| 番外 | OOM 排查与 MAT / JVisualVM | dump 分析、GC Roots 追引用 |
| 终章 | 对象创建的 7 个步骤 | 类加载 → 内存分配 → init |
这套知识到 2026 年依然有效——JVM 的核心设计几十年来没大变。变的只是 GC 算法(G1 → ZGC → Shenandoah)和一些默认参数。新人读这套笔记,再去看 2020 年后的 ZGC 调优指南,会发现基础一点没变。