字节码视角:对象创建过程
从源代码到字节码
这一节,我们深入到字节码层面,看看对象创建到底经历了什么。
new 的字节码指令
简单示例
java
public class SimpleObject {
public static void main(String[] args) {
Object obj = new Object();
}
}用 javap -c SimpleObject.class 查看字节码:
java
public static void main(java.lang.String[]);
Code:
0: new #2 // new Object,分配内存
3: dup // 复制栈顶引用
4: invokespecial #3 // 调用 Object.<init>
7: astore_1 // 存入局部变量
8: return逐条解析
0: new #2
├── 创建 Object 实例
├── 在堆中分配内存
├── 字段初始化为零值(name=null, age=0)
└── 对象引用压入操作数栈顶
3: dup
└── 复制栈顶引用
原因:一个引用传给 astore_1 存到变量
另一个引用传给 invokespecial 调用构造器
4: invokespecial #3
└── 调用 Object 的 <init> 方法(构造器)
消耗一个引用(被构造器使用)
7: astore_1
└── 存入局部变量表 slot 1
此时操作数栈顶的引用被保存
8: return
└── main 方法结束为什么需要 dup 指令
没有 dup,调用 invokespecial 时引用会被消耗,无法再存入局部变量:
没有 dup 时:
new Object() → 引用入栈
invokespecial → 引用被消耗,调用构造器
astore_1 → 操作数栈为空,无法存到变量 ❌
有 dup 时:
new Object() → 引用入栈
dup → 复制一个引用,现在栈上有 2 个引用
invokespecial → 消耗一个引用调用构造器
astore_1 → 另一个引用存入局部变量 ✅有参构造器的字节码
java
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
Student s = new Student("Alice", 20);
}
}字节码:
java
public static void main(java.lang.String[]);
Code:
0: new #7 // new Student,分配内存
3: dup
4: ldc #8 // 加载字符串 "Alice"
6: bipush 20 // 加载 int 20
8: invokespecial #9 // 调用 Student(String, int) 构造器
11: astore_1 // 存入局部变量
12: return参数传递的顺序:引用压栈 → 参数依次压栈:
栈变化:
0: new #7 → 栈:[Student 引用]
3: dup → 栈:[引用, 引用]
4: ldc #8 → 栈:[引用, 引用, "Alice"]
6: bipush 20 → 栈:[引用, 引用, "Alice", 20]
8: invokespecial → 消耗 3 个参数 + 1 个引用,调用构造器
11: astore_1 → 存到局部变量构造器的字节码
Student 的构造器编译后是什么样的?
java
public Student(java.lang.String, int);
Code:
0: aload_0 // 加载 this
1: invokespecial #1 // 调用父类 Object.<init>
4: aload_0 // 加载 this
5: ldc #2 // 加载 "Alice"(从 main 的局部变量 slot)
7: putfield #3 // this.name = "Alice"
10: aload_0 // 加载 this
11: bipush 20 // 加载 20
13: putfield #4 // this.age = 20
16: return构造器的字节码规律:
- 先调用
invokespecial Object.<init>(父类构造器) - 然后执行子类自己的字段赋值
- 最后
return
对象创建的完整流程(字节码 + JVM 内部过程)
new #7
│
▼
① 检查类是否已加载
ClassLoader → Loading → Linking → Initialization
│
▼
② 堆中分配内存
TLAB / 指针碰撞 / 空闲列表
│
▼
③ 零值初始化
name = null, age = 0
│
▼
④ 设置对象头
Mark Word + 类型指针(klass pointer)
│
▼
invokespecial #<init>
│
▼
⑤ 执行构造器字节码
<init> 方法字节码被执行
字段被赋予真正的初始值
│
▼
astore_1
│
▼
⑥ 返回对象引用多线程环境下的对象创建
面试中常问:多线程环境下如何安全地创建对象?
方案一:双重检查锁定(DCL)
java
public class LazySingleton {
private static volatile LazySingleton instance;
public static LazySingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (LazySingleton.class) {
if (instance == null) { // 第二次检查
instance = new LazySingleton();
// 问题在这里:instance = new LazySingleton()
// 字节码:new → dup → invokespecial → astore
// 可能发生重排序:先赋值引用,再执行构造器
// 导致其他线程看到非 null 但未构造完成的对象
}
}
}
return instance;
}
}volatile 的关键作用:禁止构造器 invokespecial 和引用赋值 astore 的重排序。
方案二:静态内部类
java
public class InnerClassSingleton {
private InnerClassSingleton() {}
private static class Holder {
static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return Holder.INSTANCE;
}
}原理:类的初始化由 JVM 保证线程安全,Holder.INSTANCE 的访问不会触发 DCL 的问题。
方案三:枚举
java
public enum EnumSingleton {
INSTANCE;
private EnumSingleton() {}
}枚举的构造器由 JVM 保证只执行一次,且线程安全。
本节小结
对象创建的字节码核心要点:
| 字节码 | 作用 | 关键点 |
|---|---|---|
new | 分配内存,零值初始化 | 对象头在此时设置 |
dup | 复制引用 | 避免引用被构造器消耗 |
invokespecial <init> | 执行构造器 | 先调用父类构造器 |
astore | 存入局部变量 | 引用保存 |
多线程创建对象的本质问题:指令重排序可能让引用在构造器完成前被赋值。
解决:volatile、静态内部类、枚举。
下一节,我们来看 对象内存布局与访问定位,理解对象在堆中的内部结构。
