Java对象创建过程
前言
在Java开发中,我们经常使用new
关键字来创建对象,但你是否想过,当执行Person person = new Person()
这行代码时,JVM底层究竟发生了什么?让我们看看对象是怎么被创建的。
对象创建的六个核心步骤
1. 类加载检查
当JVM执行引擎遇到new
指令时,首先会进行类加载检查:
Person person = new Person();
JVM会执行以下检查:
- 在常量池中定位到
Person
类的符号引用 - 检查
Person
类是否已经被加载 - 检查
Person
类是否已经被解析 - 检查
Person
类是否已经被初始化
如果任何一个步骤没有完成,JVM会先执行相应的类加载过程。
2. 分配内存空间
类加载检查通过后,JVM开始为新对象分配内存。对象所需的内存大小在类加载完成后就已经确定。
2.1 两种分配方式
方式一:指针碰撞
定义:假设Java堆中内存是绝对规整的,所有使用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针为分界点的指示器,分配内存就是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。
方式二:空闲列表
定义:假设Java堆中内存并不是规整的,已被使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
2.2 选择依据
if (垃圾收集器支持压缩整理) {使用指针碰撞方式();
} else {使用空闲列表方式();
}
3. 并发安全处理
对象创建是高频操作,必须保证线程安全。JVM提供两种解决方案:
3.1 CAS + 失败重试
public Object allocateMemory(int size) {do {Object current = heapPointer.get();Object next = current + size;if (heapPointer.compareAndSet(current, next)) {return current;}// CAS失败,重试} while (true);
}
3.2 TLAB
每个线程在Java堆中预先分配一块私有内存区域,避免多线程竞争。
TLAB的优势:
- 减少线程间同步开销
- 提高内存分配效率
- 支持快速的对象分配
4. 内存初始化
public class Person {private String name; // 初始化为nullprivate int age; // 初始化为0private boolean active; // 初始化为false
}
JVM将分配的内存空间(除对象头外)全部初始化为零值,这确保了实例字段在未显式赋值时也有确定的初始值。
各类型的零值:
数据类型 | 零值 |
---|---|
boolean | false |
byte | (byte)0 |
short | (short)0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | '\u0000' |
引用类型 | null |
5. 设置对象头
对象头是JVM管理对象的关键数据结构,包含两部分:
5.1 Mark Word
在64位JVM中,Mark Word占用8字节,存储: 对象哈希码、分代年龄、锁标志位、线程ID、时间戳和偏向锁标志。
Mark Word在不同锁状态下的存储内容:
锁状态 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit |
---|---|---|---|---|---|---|
无锁状态 | unused | hashcode | unused | 分代年龄 | 0 | 01 |
偏向锁 | ThreadID(54bit) | Epoch(2bit) | unused | 分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针(62bit) | unused | 分代年龄 | unused | 00 | |
重量级锁 | 指向互斥量的指针(62bit) | unused | 分代年龄 | unused | 10 | |
GC标记 | CMS过程用的标记信息(62bit) | unused | 分代年龄 | unused | 11 |
5.2 类型指针
类型指针指向对象的类元数据,JVM通过这个指针确定对象是哪个类的实例。在64位JVM中,类型指针通常占用8字节,但开启压缩指针后可压缩至4字节。
5.3 对象头示例
普通对象的对象头结构:
public class Person {private String name;private int age;
}
对于上述Person对象,其对象头包含:
组成部分 | 大小(64位JVM) | 内容描述 |
---|---|---|
Mark Word | 8字节 | 哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳 |
类型指针 | 4字节(压缩)/8字节 | 指向Person.class的类元数据信息 |
对齐填充 | 0-7字节 | 确保对象大小为8字节的倍数 |
数组对象的对象头结构:
数组对象除了Mark Word和类型指针外,还有额外的4字节存储数组长度:
int[] array = new int[10];
组成部分 | 大小(64位JVM) | 内容描述 |
---|---|---|
Mark Word | 8字节 | 对象标记信息 |
类型指针 | 4字节(压缩)/8字节 | 指向int[]的类元数据 |
数组长度 | 4字节 | 存储数组的长度(10) |
对齐填充 | 根据需要 | 保证对象大小对齐 |
6. 执行构造函数
public class Person {private String name;private int age;// 编译器生成的<init>方法public Person(String name, int age) {super(); // 调用父类构造器this.name = name; // 实例字段初始化this.age = age; // 实例字段初始化// 构造函数体逻辑}
}
构造函数执行的详细过程:
- 隐式调用父类构造器:如果没有显式调用
super()
,编译器会自动添加super()
调用 - 实例字段初始化:按照在类中声明的顺序执行字段初始化
- 执行构造函数体:执行构造函数中的自定义逻辑