对象创建流程详解
概述
Java对象的创建是一个复杂而精妙的过程。当执行new指令时,JVM会经历五个关键步骤:
graph LR
A["new指令"] --> B["Step1<br/>类加载检查"]
B --> C["Step2<br/>分配内存"]
C --> D["Step3<br/>初始化零值"]
D --> E["Step4<br/>设置对象头"]
E --> F["Step5<br/>执行init方法"]
style A fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
style B fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10
style C fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10
style D fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10
style E fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10
style F fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10建议能够默写出这五个步骤,并理解每步虚拟机做了什么。
Step1:类加载检查
虚拟机遇到new指令时,首先进行类加载检查:
- 检查这个指令的参数能否在常量池中定位到类的符号引用
- 检查这个符号引用代表的类是否已被加载、解析和初始化
如果类尚未加载,必须先执行类加载过程。
// new指令触发类加载检查
User user = new User(); // 如果User类未加载,先执行类加载graph TB
A["遇到new指令"] --> B{"类已加载?"}
B -->|"是"| C["进入Step2"]
B -->|"否"| D["执行类加载"]
D --> E["加载"]
E --> F["验证"]
F --> G["准备"]
G --> H["解析"]
H --> I["初始化"]
I --> C
style D fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10
style C fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10Step2:分配内存
类加载检查通过后,JVM在堆中为对象分配内存。对象所需内存大小在类加载完成后就已确定。
分配方式
分配方式取决于Java堆是否规整,而堆是否规整又取决于GC收集器是否带压缩整理功能:
指针碰撞(Bump The Pointer)
适用场景:堆内存规整(无内存碎片)
原理:用过的内存放一边,未用的放另一边,中间一个指针作为分界。分配时只需移动指针即可。
graph LR
subgraph "指针碰撞"
Used["已使用内存"]
Pointer["指针"]
Free["空闲内存"]
NewObj["新对象"]
end
Used --> Pointer
Pointer -->|"移动"| NewObj
NewObj --> Free
style Used fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10
style Pointer fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
style Free fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10使用的GC:Serial、ParNew等
空闲列表(Free List)
适用场景:堆内存不规整(有内存碎片)
原理:维护一个列表,记录哪些内存块可用。分配时从列表中找足够大的内存块。
graph TB
subgraph "空闲列表"
List["空闲块列表"]
B1["块1: 512B"]
B2["块2: 1024B"]
B3["块3: 256B"]
end
List --> B1
List --> B2
List --> B3
style List fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
style B2 fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10使用的GC:CMS等
并发安全保障
对象创建是高频操作,在并发环境下需要保证线程安全。HotSpot提供两种方式:
CAS + 失败重试
使用CAS(Compare And Swap)保证更新操作的原子性,失败则重试。
// CAS伪代码
do {
oldValue = memoryPointer;
newValue = oldValue + objectSize;
} while (!CAS(memoryPointer, oldValue, newValue));TLAB(Thread Local Allocation Buffer)
为每个线程在Eden区预先分配一小块私有内存(TLAB),线程优先在自己的TLAB中分配对象,无需同步。
graph TB
subgraph "Eden区"
TLAB1["线程1 TLAB"]
TLAB2["线程2 TLAB"]
TLAB3["线程3 TLAB"]
Shared["共享区域"]
end
T1["线程1"] --> TLAB1
T2["线程2"] --> TLAB2
T3["线程3"] --> TLAB3
style TLAB1 fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10
style TLAB2 fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
style TLAB3 fill:#DDA0DD,stroke:#8B008B,stroke-width:2px,rx:10,ry:10TLAB配置:
# 启用TLAB(默认开启)
-XX:+UseTLAB
# 设置TLAB大小
-XX:TLABSize=256kStep3:初始化零值
内存分配完成后,虚拟机将分配的内存空间初始化为零值(不包括对象头)。
这保证了对象的实例字段在Java代码中不赋初始值就能直接使用。
public class User {
private int age; // 初始化为0
private String name; // 初始化为null
private boolean active; // 初始化为false
private double balance; // 初始化为0.0
public void printAge() {
System.out.println(age); // 输出0,不会报错
}
}各类型的零值:
| 类型 | 零值 |
|---|---|
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0d |
| boolean | false |
| char | '\u0000' |
| 引用类型 | null |
Step4:设置对象头
虚拟机对对象进行必要的设置,这些信息存放在对象头(Object Header):
- 对象是哪个类的实例
- 如何找到类的元数据信息
- 对象的哈希码(懒加载)
- 对象的GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
graph TB
subgraph "对象头内容"
MW["Mark Word"]
KP["Klass Pointer"]
end
subgraph "Mark Word详情"
Hash["HashCode"]
Age["GC年龄(4bit)"]
Lock["锁标志"]
Thread["线程ID"]
end
MW --> Hash
MW --> Age
MW --> Lock
MW --> Thread
style MW fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
style KP fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10Step5:执行init方法
从虚拟机视角,对象已经创建完成。
但从Java程序视角,对象创建才刚开始。执行new指令后会接着执行<init>方法,按程序员的意愿初始化对象。
public class User {
private String name;
private int age;
// <init>方法由构造方法生成
public User(String name, int age) {
this.name = name; // Step5执行
this.age = age; // Step5执行
}
}<init>方法包含:
- 成员变量的显式赋值
- 构造代码块
- 构造方法体
public class InitOrderDemo {
// 1. 成员变量显式赋值
private int value = 10;
// 2. 构造代码块
{
System.out.println("构造代码块执行");
}
// 3. 构造方法
public InitOrderDemo() {
System.out.println("构造方法执行");
}
}创建过程时序
sequenceDiagram
participant Code as Java代码
participant JVM as JVM
participant Heap as 堆内存
participant Method as 方法区
Code->>JVM: new User()
JVM->>Method: 类加载检查
Method-->>JVM: 类已加载
JVM->>Heap: 分配内存
Heap-->>JVM: 返回内存地址
JVM->>Heap: 初始化零值
JVM->>Heap: 设置对象头
JVM->>Code: 执行<init>方法
Code->>Heap: 初始化实例变量
Heap-->>Code: 返回对象引用对象创建优化
逃逸分析优化
如果对象未逃逸出方法,JVM可能进行优化:
// 未逃逸,可能被优化
public void process() {
Point p = new Point(1, 2);
int sum = p.x + p.y;
System.out.println(sum);
}可能的优化:
- 栈上分配:对象在栈上分配,方法结束自动回收
- 标量替换:对象拆解为基本类型,不创建对象
TLAB优化
大部分对象在TLAB中分配,无需同步:
# 打印TLAB信息
-XX:+PrintTLAB
# 输出示例
# TLAB: gc thread: 0x00007f8b1800a800 [id: 12345]
# desired_size: 1048576KB slow allocs: 5对象创建的字节码
public class User {
private String name;
public User(String name) {
this.name = name;
}
}对应的字节码:
0: new #2 // class User
3: dup // 复制栈顶引用
4: aload_1 // 加载参数
5: invokespecial #3 // 调用<init>方法
8: areturn // 返回引用| 指令 | 说明 |
|---|---|
| new | 分配内存,执行Step1-4 |
| dup | 复制引用(一个用于赋值,一个用于调用构造方法) |
| invokespecial | 执行<init>方法,即Step5 |
常见问题
对象一定在堆上分配吗?
不一定。通过逃逸分析,未逃逸对象可能:
- 栈上分配
- 标量替换(不创建对象)
分配内存时如何保证线程安全?
两种方式:
- CAS + 失败重试
- TLAB(线程私有缓冲区)
Step3零值初始化的意义?
保证对象字段有默认值,不需要显式初始化就能使用。
理解对象创建过程,对于分析性能问题、理解内存模型非常重要。
更新: 2025-12-06 15:11:03
原文: https://www.yuque.com/u22210564/zoxfmt/obumf97a4ke5u6mu