Contents

JVM基础:Java对象的创建与内存布局:你以为的 new 不止 new

你真的理解 new 吗?

“Java 中创建一个对象,不就是 new 一下吗?”这是许多开发者对对象创建的第一印象。但如果你曾经在生产环境遇到内存泄漏、对象占用过大,或者面试时被问“对象在内存中长什么样”,你可能就会意识到,事情没有那么简单。

今天,我们不谈语法,不谈封装继承多态,我们只谈:一个 Java 对象,在 JVM 中是怎么被创建出来的?它在内存里究竟长什么样?


一、对象是怎么被创建的?——JVM 的对象创建流程

Java中常见的 5 种对象创建方式:

  1. new 关键字(最常见)
  2. 反射 Class.newInstance()Constructor.newInstance()
  3. 克隆 clone()
  4. 反序列化 ObjectInputStream.readObject()
  5. 直接使用 Unsafe.allocateInstance()(跳过构造方法)

无论哪种方式,底层最终都会调用 JVM 的对象分配逻辑

我们就以最常见的 new 来展开。


对象创建流程:JVM 做了什么?

当你执行:

User user = new User();

JVM 做了下面这 6 步:


第一步:检查类是否已加载、初始化

JVM 首先会确保 User 类已经:

  • 被加载(类加载器已加载 class 文件) // todo: 类加载器以及双亲委派、类加载的五大阶段
  • 被验证/准备/解析/初始化(前文讲过五大阶段)

如果没有,JVM 会先加载类,执行 <clinit> 方法,完成类初始化。


第二步:在堆中分配内存

JVM 会在堆中为对象分配一块内存,分为两种策略:

  • 指针碰撞(Bump the Pointer):内存连续,分配效率高(依赖 GC 的压缩整理)
  • 空闲列表(Free List):内存不连续时使用,类似链表管理

JVM 会根据当前堆的内存布局决定用哪种策略。

如果多个线程同时竞争堆内存,则需要加锁保证线程安全,效率低些。为了解决这个问题,我们可以引入:

  • TLAB(Thread Local Allocation Buffer):如果开启了 TLAB,那么每个线程有一块私有分配区,分配内存时只需在线程内部 bump 指针即可,效率极高。 当然TLAB也有其缺点和问题,后面我们会专门写文章探讨。 // todo 多线程下的内存分配机制

JVM 参数:-XX:+UseTLAB 可以控制是否启用 TLAB。


第三步:对象内存初始化为零值(默认值)

所有字段初始化为默认值(int 为 0,boolean 为 false,引用为 null)。

⚠️ 注意,这一步不是执行构造方法,仅做内存层面的初始化。


第四步:设置对象头

对象头会包括:

  • Mark Word:哈希码、GC 标记、锁信息
  • Klass Pointer:指向对象的类型元数据(即 class 的方法区结构)
  • 如果是数组,还会多一个数组长度字段

这一步完成后,对象已经拥有类型、状态信息。


第五步:执行 <init> 构造方法

JVM 调用 <init> 方法,按照你定义的构造器逻辑,初始化对象的实际字段值。

此时,Java 代码层面看到的初始化操作就开始发生。


第六步:返回对象引用

创建完成后,返回堆中对象的引用,赋值给变量 user

如果开启了逃逸分析且确定对象不会逃逸线程作用域,JVM 甚至会将对象分配在栈上(标量替换)—— 即所谓栈上分配


三、Java 对象在内存中的真实结构

对象在代码中看起来是这样的:

User user = new User();

但在内存中,它其实是一块结构严谨、精心排布的数据区域。要真正理解对象在 JVM 里的模样,我们要从对象头实例数据区、以及对齐填充区这三部分说起。


1. 对象头(Header)

对象头是每个 Java 对象都必须携带的一部分,主要由两项组成:

👉 Mark Word(标记字段)

这是对象头中的前 8 个字节(在 64 位 JVM 且开启压缩指针的情况下),它用于存储以下信息: // todo 详解指针压缩

状态含义
hashCode对象的 hash 值
GC 分代信息对象的年龄(用于 GC)
锁标记当前对象的锁状态
时间戳可能用于偏向锁时间记录

⚠️ 注意:当对象加锁时,Mark Word 会被替换成锁记录、指针等内容,hashCode 信息可能丢失或延迟计算。 // todo 加锁过程详解以及锁的分类和升级

👉 Klass Pointer(类型指针)

  • 占用 4 或 8 字节(取决于是否开启 CompressedClassPointers)。
  • 指向方法区中的类元信息,表明这个对象是哪个类的实例。
  • 也就是通过它,JVM 知道你调用的方法来自哪个类。

👉(可选)数组长度字段

如果是数组对象,对象头还会多一个 length 字段,记录数组的长度。


2. 实例数据区(Instance Data)

紧接着对象头的,就是你在 Java 类中定义的实例变量,比如:

class User {
    int id;
    String name;
}

这些成员变量会被按照以下原则依次排布:

  • 类型相同的变量可能会被紧凑排列以节省空间,这叫做字段重排序。 // todo 详解字段重排,以及所产生的问题
  • Java 源码中字段的顺序,不一定等于内存中的排列顺序。
  • 父类字段会被排在子类字段之前。

举个例子,下面这个类:

class Example {
    boolean a;
    int b;
    boolean c;
}

其实际内存排列可能是:

[boolean a] [boolean c] [padding] [int b]

为了对齐 int 类型,JVM 会插入 padding 字节,使字段地址按 4 字节或 8 字节对齐。


3. 对齐填充区(Padding)

JVM 为了提高 CPU 访问效率,要求对象的总大小必须是 8 字节的倍数。如果对象的头 + 实例数据不是 8 的整数倍,JVM 会在对象末尾补上填充字节。

例如:

  • 一个对象总大小为 22 字节,JVM 会补充 2 字节填充,使其变为 24 字节。

你可以通过参数 -XX:ObjectAlignmentInBytes=16 设置对齐单位(默认为 8)。


🔍 结构示意图:

┌──────────────┬──────────────────────┬────────────────────┐
│  对象头       │   实例数据(字段)     │    对齐填充         │
│ (MarkWord +  │  int、long、String等 │  padding bytes     │
│  KlassPointer)│                      │                    │
└──────────────┴──────────────────────┴────────────────────┘

✅ 小结:

Java 对象结构 = 对象头(Header) + 实例数据(Instance Data)+ 对齐填充(Padding)

  • 对象头是 JVM 内部操作的关键区域(锁、GC、hashCode 全靠它)。
  • 字段在内存中的顺序可能与你想象的不一样,字段重排序是常见优化。
  • 内存对齐规则虽然你感知不到,但它直接影响内存使用效率。

// todo 下一篇,我们将用一款神器 —— JOL(Java Object Layout),亲手验证上面讲的结构,并可视化不同字段组合带来的内存差异。