Skip to content

对象创建流程详解

概述

Java对象的创建是一个复杂而精妙的过程。当执行new指令时,JVM会经历五个关键步骤:

mermaid
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指令时,首先进行类加载检查:

  1. 检查这个指令的参数能否在常量池中定位到类的符号引用
  2. 检查这个符号引用代表的类是否已被加载、解析和初始化

如果类尚未加载,必须先执行类加载过程。

java
// new指令触发类加载检查
User user = new User();  // 如果User类未加载,先执行类加载
mermaid
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:10

Step2:分配内存

类加载检查通过后,JVM在堆中为对象分配内存。对象所需内存大小在类加载完成后就已确定。

分配方式

分配方式取决于Java堆是否规整,而堆是否规整又取决于GC收集器是否带压缩整理功能

指针碰撞(Bump The Pointer)

适用场景:堆内存规整(无内存碎片)

原理:用过的内存放一边,未用的放另一边,中间一个指针作为分界。分配时只需移动指针即可。

mermaid
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)

适用场景:堆内存不规整(有内存碎片)

原理:维护一个列表,记录哪些内存块可用。分配时从列表中找足够大的内存块。

mermaid
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)保证更新操作的原子性,失败则重试。

java
// CAS伪代码
do {
    oldValue = memoryPointer;
    newValue = oldValue + objectSize;
} while (!CAS(memoryPointer, oldValue, newValue));

TLAB(Thread Local Allocation Buffer)

为每个线程在Eden区预先分配一小块私有内存(TLAB),线程优先在自己的TLAB中分配对象,无需同步。

mermaid
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:10

TLAB配置

bash
# 启用TLAB(默认开启)
-XX:+UseTLAB

# 设置TLAB大小
-XX:TLABSize=256k

Step3:初始化零值

内存分配完成后,虚拟机将分配的内存空间初始化为零值(不包括对象头)。

这保证了对象的实例字段在Java代码中不赋初始值就能直接使用。

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,不会报错
    }
}

各类型的零值

类型零值
int0
long0L
float0.0f
double0.0d
booleanfalse
char'\u0000'
引用类型null

Step4:设置对象头

虚拟机对对象进行必要的设置,这些信息存放在对象头(Object Header)

  • 对象是哪个类的实例
  • 如何找到类的元数据信息
  • 对象的哈希码(懒加载)
  • 对象的GC分代年龄
  • 锁状态标志
  • 线程持有的锁
  • 偏向线程ID
  • 偏向时间戳
mermaid
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:10

Step5:执行init方法

虚拟机视角,对象已经创建完成。

但从Java程序视角,对象创建才刚开始。执行new指令后会接着执行<init>方法,按程序员的意愿初始化对象。

java
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>方法包含:

  • 成员变量的显式赋值
  • 构造代码块
  • 构造方法体
java
public class InitOrderDemo {
    // 1. 成员变量显式赋值
    private int value = 10;
    
    // 2. 构造代码块
    {
        System.out.println("构造代码块执行");
    }
    
    // 3. 构造方法
    public InitOrderDemo() {
        System.out.println("构造方法执行");
    }
}

创建过程时序

mermaid
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可能进行优化:

java
// 未逃逸,可能被优化
public void process() {
    Point p = new Point(1, 2);
    int sum = p.x + p.y;
    System.out.println(sum);
}

可能的优化:

  • 栈上分配:对象在栈上分配,方法结束自动回收
  • 标量替换:对象拆解为基本类型,不创建对象

TLAB优化

大部分对象在TLAB中分配,无需同步:

bash
# 打印TLAB信息
-XX:+PrintTLAB

# 输出示例
# TLAB: gc thread: 0x00007f8b1800a800 [id: 12345] 
# desired_size: 1048576KB slow allocs: 5

对象创建的字节码

java
public class User {
    private String name;
    
    public User(String name) {
        this.name = name;
    }
}

对应的字节码:

plain
0: new           #2    // class User
3: dup                 // 复制栈顶引用
4: aload_1             // 加载参数
5: invokespecial #3    // 调用<init>方法
8: areturn             // 返回引用
指令说明
new分配内存,执行Step1-4
dup复制引用(一个用于赋值,一个用于调用构造方法)
invokespecial执行<init>方法,即Step5

常见问题

对象一定在堆上分配吗?

不一定。通过逃逸分析,未逃逸对象可能:

  • 栈上分配
  • 标量替换(不创建对象)

分配内存时如何保证线程安全?

两种方式:

  1. CAS + 失败重试
  2. TLAB(线程私有缓冲区)

Step3零值初始化的意义?

保证对象字段有默认值,不需要显式初始化就能使用。

理解对象创建过程,对于分析性能问题、理解内存模型非常重要。

更新: 2025-12-06 15:11:03
原文: https://www.yuque.com/u22210564/zoxfmt/obumf97a4ke5u6mu

Java 后端面试知识库