对象在 JVM 中的生命周期:从创建到访问的完整历程
在 Java 中,“万物皆对象”,对象是程序运行的核心载体。从对象的创建、内存分配,到在 JVM 中的存储布局,再到如何被访问,每一步都遵循 JVM 的严格规范。本文将详细解析对象在 JVM 中的 “一生”,包括创建方式、JVM 处理流程、内存布局及访问定位机制。
对象的创建方式
Java 中创建对象的方式多样,不同方式对应不同的底层实现,核心区别在于是否依赖构造器、权限要求等:
创建方式 | 核心原理 | 特点与限制 |
---|---|---|
new 关键字 |
直接调用类的构造器 | 最常用,可调用任意权限的构造器(public/protected/private)。 |
Class.newInstance() |
反射调用无参构造器 | 仅支持无参构造器,且构造器必须为 public (JDK 9+ 后放宽限制,但仍需可访问)。 |
Constructor.newInstance() |
反射调用指定构造器 | 支持任意参数的构造器,无权限限制(即使构造器为 private ,也可通过反射访问)。 |
clone() 方法 |
复制已有对象的内存数据 | 需实现 Cloneable 接口(否则抛 CloneNotSupportedException ),不调用构造器。 |
反序列化 | 从字节流恢复对象 | 需类实现 Serializable 接口,不调用构造器,通过输入流重建对象。 |
JVM 中对象的创建步骤
以最常见的 new TestObject()
为例,其在 JVM 中的创建过程可分为 6 个核心步骤,对应字节码指令如下:
1 | 0: new #2 // 步骤1:检查类并分配内存 |
步骤 1:类加载检查(new
指令)
new
指令首先触发类加载检查:
- 从常量池查找
TestObject
的符号引用,检查该类是否已完成加载、链接、初始化; - 若未加载,则通过类加载器(双亲委派机制)加载类文件,生成
Class
对象(存储在方法区); - 若类加载失败(如类不存在),抛出
NoClassDefFoundError
。
步骤 2:为对象分配内存
类加载完成后,JVM 需为对象分配内存(大小在类加载时已确定)。内存分配有两种算法,取决于堆内存是否规整:
算法 | 适用场景 | 核心逻辑 |
---|---|---|
指针碰撞 | 堆内存规整(如 Serial、ParNew 收集器) | 用一个指针划分已用 / 空闲内存,分配时指针向空闲区移动对象大小的距离。 |
空闲列表 | 堆内存不规整(如 CMS 收集器) | 维护一张空闲内存块列表,分配时从列表中找到足够大的块划分给对象。 |
解决并发分配问题
对象创建频繁,可能出现多线程同时分配内存的冲突,JVM 采用两种解决方案:
- CAS 同步:对内存分配操作加锁(Compare-And-Swap),保证原子性;
- TLAB(本地线程分配缓冲):为每个线程预分配一小块 Eden 区内存(默认占 Eden 的 1%),线程优先在自己的 TLAB 中分配,用完后再竞争公共内存(减少锁竞争)。可通过
-XX:+UseTLAB
开启(默认开启)。
步骤 3:初始化默认值
内存分配后,JVM 会将对象的实例变量设为默认值(零值):
- 基本类型:如
int
设为 0,boolean
设为false
; - 引用类型:设为
null
。
这一步确保对象在未显式初始化时,实例变量也有合法值(对应类加载的 “准备阶段” 逻辑)。
步骤 4:设置对象头(mark word
)
对象头是对象内存的核心元数据,包含两部分:
- Mark Word(标记字段):存储对象运行时状态,如:
- 哈希值(
hashCode
); - GC 分代年龄(经历 Minor GC 的次数);
- 锁状态标志(无锁、偏向锁、轻量级锁、重量级锁);
- 偏向线程 ID(偏向锁时)。
- 哈希值(
- 类型指针:指向对象所属类的
Class
对象(在方法区),JVM 通过该指针确定对象的类型(如TestObject.class
)。
步骤 5:执行 <init>
方法(invokespecial
指令)
invokespecial
指令调用类的构造器 <init>
方法,完成显式初始化:
- 执行实例变量的显式赋值(如
private int a = 10
); - 执行实例代码块(
{ ... }
); - 执行构造器中的逻辑。
这一步将对象从 “默认初始化状态” 转为 “用户期望的初始化状态”。
步骤 6:赋值给引用变量
最后,将对象的引用(内存地址)存入虚拟机栈的局部变量表(如 astore_1
指令将引用存入 Slot 1),至此对象创建完成。
对象的内存布局
对象在堆中的内存布局分为三部分:对象头、实例数据、对齐填充,以 64 位 HotSpot 为例:
1. 对象头(Header)
- Mark Word:8 字节(64 位),存储运行时状态(如锁信息、GC 年龄);
- 类型指针:8 字节(64 位,开启指针压缩后为 4 字节),指向
Class
对象; - 数组长度(仅数组对象):4 字节,记录数组元素个数(非数组对象无此字段)。
可以使用该方式来查看对象头
1
2
3
4
5
6 <!--查看对象头工具-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
1 ClassLayout.parseClass(TestDup.class).toPrintable()
2. 实例数据(Instance Data)
对象的核心数据,存储实例变量(非静态变量),包括:
- 从父类继承的实例变量;
- 自身定义的实例变量。
存储顺序由 JVM 优化决定,默认按 “longs/doubles → ints/shorts/chars → bytes/booleans → references” 排序,以减少内存对齐开销。
3. 对齐填充(Padding)
JVM 要求对象总长度必须是 8 字节的整数倍(内存对齐),若对象头 + 实例数据的长度不足,则用对齐填充补全(无实际意义,仅满足对齐要求)。
示例:一个简单对象的内存布局(64 位,指针压缩开启)
1 | TestObject 对象(总大小 16 字节): |
对象的访问定位
创建对象后,Java 程序通过栈中的引用(reference) 访问堆中的对象,JVM 有两种访问方式:
1. 句柄访问
- 原理:堆中划分一块 “句柄池”,引用存储句柄地址;句柄包含两部分指针:
- 实例数据指针:指向对象的实例数据(堆中);
- 类型数据指针:指向对象的类元数据(方法区中)。
- 优点:对象被 GC 移动时(如压缩整理),只需修改句柄的实例数据指针,引用本身不变,稳定且安全。
- 缺点:多一次指针跳转,访问效率略低。
2. 直接指针访问(HotSpot 采用)
- 原理:引用直接存储对象在堆中的内存地址,对象头的类型指针直接指向类元数据。
- 优点:少一次指针跳转,访问速度更快(对象访问频繁,可节省大量开销)。
- 缺点:对象移动时,需同时修改所有引用的地址(依赖 GC 过程中的指针更新)。
v1.3.10