Skip to content

字节码视角:对象创建过程

从源代码到字节码

这一节,我们深入到字节码层面,看看对象创建到底经历了什么。

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

构造器的字节码规律:

  1. 先调用 invokespecial Object.<init>(父类构造器)
  2. 然后执行子类自己的字段赋值
  3. 最后 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、静态内部类、枚举。

下一节,我们来看 对象内存布局与访问定位,理解对象在堆中的内部结构。

基于 VitePress 构建