JVM 对象创建的核心流程! - 实践

在这里插入图片描述

在Java中,我们最常见且最主要的创建对象方式就是使用 new 关键字。当程序执行到 new 指令时,JVM会经历一系列复杂而精确的步骤来完成对象的创建。
在这里插入图片描述


JVM 对象创建的核心流程

JVM 对象创建的核心流程大致可以分为以下几个阶段:

  1. 类加载检查 (Class Loading Check)
  2. 内存分配 (Memory Allocation)
  3. 初始化零值 (Initialization to Zero)
  4. 设置对象头 (Setting Object Header)
  5. 执行 <init> 方法 (Executing Constructor)
  6. 返回对象引用 (Returning Object Reference)

我们来逐一详细讲解每个阶段。

1. 类加载检查 (Class Loading Check)

当JVM遇到一条 new 指令时,它首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。

  • 如果尚未加载: JVM会触发类的加载(Loading)、连接(Linking)和初始化(Initialization)过程。这个过程就是将类的.class文件字节码加载到内存,并进行验证、准备、解析、初始化等操作,最终在JVM的方法区(Java 8及以后称为Metaspace)中生成该类的运行时数据结构。
  • 如果已加载: JVM会直接跳过类加载阶段,进入下一步。

代码示例:
当你写下 User user = new User();,JVM首先确保 User 类已经被加载。

// User.java
public class User
{
private String name;
private int age;
public User() {
System.out.println("User类构造函数被调用!");
this.name = "DefaultName";
this.age = 18;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void sayHello() {
System.out.println("Hello, I'm " + name + ", " + age + " years old.");
}
}
// Main.java
public class Main
{
public static void main(String[] args) {
System.out.println("--- 准备创建User对象 ---");
// 第一次创建User对象,会触发User类的加载、连接、初始化。
// 如果User类之前未被加载过。
User user1 = new User();
System.out.println("User1创建完成。");
user1.sayHello();
System.out.println("\n--- 再次创建User对象 ---");
// 第二次创建User对象时,User类已经被加载,跳过类加载阶段。
User user2 = new User();
System.out.println("User2创建完成。");
user2.sayHello();
}
}

输出示例:

--- 准备创建User对象 ---
User类构造函数被调用!
User1创建完成。
Hello, I'm DefaultName, 18 years old.
--- 再次创建User对象 ---
User类构造函数被调用!
User2创建完成。
Hello, I'm DefaultName, 18 years old.

说明: 尽管构造函数被调用了两次,但 User 类的加载只会发生一次。首次 new User() 会触发类加载,第二次就不会了。

2. 内存分配 (Memory Allocation)

类加载检查通过后,JVM将为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确定。为对象分配内存的任务其实就是把一块确定大小的内存从Java堆(Heap)中划分出来。

内存分配的两种主要策略:

  • 指针碰撞 (Pointer Bump):

    • 适用场景: 堆内存是规整的,即所有使用过的内存放在一边,空闲的内存放在另一边,中间由一个指针作为分界点的指示器。
    • 工作原理: 分配内存时,只需要把那个指针向空闲空间那边挪动一段与对象大小相等的距离即可。
    • GCs: Serial、ParNew等带有Compacting(压缩)过程的垃圾收集器,通常会整理堆内存,使其保持规整。
  • 空闲列表 (Free List):

    • 适用场景: 堆内存是不规整的,内存中已使用的和未使用的内存是交织在一起的,就无法使用指针碰撞。
    • 工作原理: 虚拟机需要维护一个列表,记录哪些内存块是可用的。分配时,从列表中找到足够大的空间划分给对象实例,并更新列表上的记录。
    • GCs: CMS(Concurrent Mark Sweep)垃圾收集器,它不对堆内存进行压缩,通常采用空闲列表。

内存分配的并发问题与解决方案:

在并发环境中,多个线程可能同时请求分配内存,这可能导致指针碰撞或空闲列表的更新操作产生竞争。JVM提供了两种解决方案:

  • CAS (Compare-and-Swap) + 失败重试:

    • 对分配内存空间的动作进行同步处理。JVM采用CAS操作,配合失败重试的方式保证更新操作的原子性。如果某个线程分配失败,会通过自旋等方式重试,直到成功。
  • TLAB (Thread Local Allocation Buffer - 线程本地分配缓冲区):

    • 最常用和高效的解决方案。 每个线程在Java堆中预先分配一小块私有内存,称为TLAB。线程大多数情况下都在自己的TLAB上分配内存,这样可以避免加锁,大大提高了分配效率。
    • 只有当TLAB用完,需要重新申请新的TLAB时,才需要进行同步锁定。
    • 可以通过 -XX:+UseTLAB 参数启用(默认开启),-XX:TLABSize 设置TLAB大小。

代码示例 (无直接分配代码,但可借GC日志观察):
无法直接代码演示内存分配,但我们可以通过JVM参数来观察GC日志,间接感知内存分配情况。

# 启动Java程序时添加JVM参数
java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps Main

通过GC日志,你可以看到类似 [PSYoungGen: 2650K->320K(9216K)] 2650K->320K(30720K) 这样的信息,表明年轻代(Young Generation)的内存使用情况,新对象的创建通常会首先在Eden区分配。

3. 初始化零值 (Initialization to Zero)

内存分配完成后,JVM会将分配到的内存空间(不包括对象头)都初始化为零值(zero-value)。

  • 基本数据类型:int 为 0,long 为 0L,booleanfalsechar\u0000 (空字符),float 为 0.0f,double 为 0.0d。
  • 引用类型:null

目的:
确保对象的实例字段在没有被程序员显式赋值的情况下,可以访问到这些数据类型对应的零值,保证程序的健壮性,避免了当你在代码中访问未初始化字段时可能出现的 NullPointerException 或乱码等问题。

代码示例:

public class DefaultValueDemo
{
private int intField;
private boolean booleanField;
private String stringField;
// 引用类型
public static void main(String[] args) {
DefaultValueDemo demo = new DefaultValueDemo();
System.out.println("intField (default): " + demo.intField);
System.out.println("booleanField (default): " + demo.booleanField);
System.out.println("stringField (default): " + demo.stringField);
}
}

输出:

intField (default): 0
booleanField (default): false
stringField (default): null

说明:demo 对象创建后,所有字段都被JVM自动初始化为零值,而无需我们手动赋初值。

4. 设置对象头 (Setting Object Header)

在零值初始化之后,JVM会对对象进行必要的设置,例如将这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(HashCode)、对象的GC分代年龄(Age)、锁状态标志等信息存放在对象头(Object Header)中。

对象头主要包含两部分信息:

  • Mark Word (标记字): 存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、偏向线程ID、偏向时间戳等。这部分数据是动态变化的,它会随着对象状态的改变而改变,例如在锁定状态下,Mark Word 会存储不同的信息。
  • Klass Pointer (类指针): 类型指针,即对象指向它的类元数据(在Metaspace中)的指针,虚拟机通过这个指针来确定该对象是哪个类的实例。在64位JVM中,如果开启了指针压缩(-XX:+UseCompressedOops,默认开启),Klass Pointer 会被压缩成32位。
5. 执行 < init > 方法 (Executing Constructor)

在以上步骤完成后,从JVM的视角看,一个新对象已经诞生了。但是对于Java应用程序来说,对象才刚刚准备好。
执行 new 指令之后会接着执行 <init> 方法(也就是我们通常所说的构造方法),按照我们程序员的意愿对对象进行初始化。

  • 作用: 执行构造方法中的业务代码,对成员变量赋初始值(覆盖掉零值),执行其他必要的初始化操作。
  • 顺序: 如果在构造方法中没有对某个字段赋值,那么它将保持在第3步中被赋予的零值。构造方法还会隐式或显式地调用父类的构造方法,以确保父类部分的初始化。

代码示例:

public class User
{
private String name;
private int age;
private String email;
// 新增字段
// 默认构造函数
public User() {
// 在这之前,name是null, age是0, email是null (零值初始化)
System.out.println("User() constructor called. name=" + this.name + ", age=" + this.age + ", email=" + this.email);
this.name = "InitializedName";
// 覆盖零值
this.age = 20;
// 覆盖零值
System.out.println("User() constructor finished. name=" + this.name + ", age=" + this.age + ", email=" + this.email);
}
// 带参数的构造函数
public User(String name, int age) {
// 在这之前,name是null, age是0, email是null (零值初始化)
this.name = name;
this.age = age;
System.out.println("User(name, age) constructor called. name=" + this.name + ", age=" + this.age);
}
public static void main(String[] args) {
System.out.println("--- Creating User 1 (default constructor) ---");
User user1 = new User();
System.out.println("User1 details: Name=" + user1.name + ", Age=" + user1.age + ", Email=" + user1.email);
System.out.println("\n--- Creating User 2 (parameterized constructor) ---");
User user2 = new User("Alice", 25);
System.out.println("User2 details: Name=" + user2.name + ", Age=" + user2.age + ", Email=" + user2.email);
}
}

输出:

--- Creating User 1 (default constructor) ---
User() constructor called. name=null, age=0, email=null
User() constructor finished. name=InitializedName, age=20, email=null
User1 details: Name=InitializedName, Age=20, Email=null
--- Creating User 2 (parameterized constructor) ---
User(name, age) constructor called. name=Alice, age=25
User2 details: Name=Alice, Age=25, Email=null

说明:

  • 在构造函数执行前,零值初始化已经完成。所以,User() 构造函数刚开始时,namenullage0emailnull
  • 构造函数中的 this.name = "InitializedName";this.age = 20; 覆盖了零值。
  • email 字段在两个构造函数中都未被显式赋值,因此它在创建后仍然保持为 null
6. 返回对象引用 (Returning Object Reference)

所有上述操作都完成后,一个完整的对象就已经在堆内存中生成,并且对象头也已经被正确设置。这时,JVM会将堆内存中对象的地址(引用)压入操作数栈,作为 new 指令的执行结果,供程序后续使用。


其他创建对象的方式

除了 new 关键字,还有几种方式可以创建对象,它们在内部机制上会跳过 new 指令的一些步骤:

  1. Class.newInstance()

    • 通过反射创建对象。调用的是无参构造函数。
    • 已被 Constructor.newInstance() 替代,在Java 9中已被废弃。
    • 缺点:会抛出受检查异常,性能不如直接 new
    • 内部流程: 检查类加载 -> 内存分配 -> 零值初始化 -> 设置对象头 -> 调用无参构造函数。
  2. Constructor.newInstance()

    • 通过反射创建对象,可以调用任意构造函数(包括带参构造函数)。
    • 内部流程: 检查类加载 -> 内存分配 -> 零值初始化 -> 设置对象头 -> 调用指定的构造函数。
    • 缺点:反射操作相对耗时,适用于需要动态创建对象的场景。
  3. Object.clone()

    • 克隆一个现有对象。此方法不会调用任何构造函数。
    • 要求类实现 Cloneable 接口。
    • 内部流程: 直接复制现有对象的内存,因此不会触发类加载检查、内存分配(而是复制现有内存)、零值初始化、构造函数执行等步骤。它仅仅是拷贝了内存中的二进制数据。
    • 缺点:深拷贝/浅拷贝问题需要注意。
  4. 序列化与反序列化:

    • 通过 ObjectInputStream.readObject() 方法从字节流中反序列化创建对象。
    • 此方法也不会调用任何构造函数。
    • 内部流程: JVM会分配内存,读取并恢复对象数据,但不会调用构造函数。
    • 缺点:安全性问题(恶意序列化数据),版本兼容性问题。

代码示例:

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
class SomeObject
implements Cloneable, Serializable {
private String value;
private int id;
public SomeObject() {
System.out.println("SomeObject() constructor called.");
this.value = "default";
this.id = 0;
}
public SomeObject(String value, int id) {
System.out.println("SomeObject(value, id) constructor called.");
this.value = value;
this.id = id;
}
public String getValue() {
return value;
}
public int getId() {
return id;
}
public void setValue(String value) {
this.value = value;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
// 浅拷贝
}
@Override
public String toString() {
return "SomeObject [value=" + value + ", id=" + id + "]";
}
}
public class OtherCreationMethods
{
public static void main(String[] args) throws InstantiationException,
IllegalAccessException, NoSuchMethodException, InvocationTargetException,
CloneNotSupportedException, IOException, ClassNotFoundException {
System.out.println("--- 1. Using new keyword ---");
SomeObject obj1 = new SomeObject("New Object", 1);
System.out.println(obj1);
System.out.println("\n--- 2. Using Class.newInstance() (Deprecated in Java 9+) ---");
// SomeObject obj2 = SomeObject.class.newInstance(); // 已废弃,但原理相同
Constructor<
SomeObject> constructorNoArgs = SomeObject.class.
getConstructor();
SomeObject obj2 = constructorNoArgs.newInstance();
System.out.println(obj2);
System.out.println("\n--- 3. Using Constructor.newInstance() (Reflection) ---");
Constructor<
SomeObject> constructorWithArgs = SomeObject.class.
getConstructor(String.class,
int.class)
;
SomeObject obj3 = constructorWithArgs.newInstance("Reflected Object", 3);
System.out.println(obj3);
System.out.println("\n--- 4. Using Object.clone() ---");
SomeObject obj4 = (SomeObject) obj1.clone();
// 基于obj1克隆
System.System.out.println("Cloned object before modify: " + obj4);
obj4.setValue("Cloned Object");
// 修改克隆对象
System.out.println("Original object after modify cloned one: " + obj1);
// obj1未受影响 (String是不可变的,int是基本类型)
System.out.println("Cloned object after modify: " + obj4);
// 注意:这里没有调用构造函数!
System.out.println("\n--- 5. Using Serialization/Deserialization ---");
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj1);
// 序列化obj1
oos.close();
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
SomeObject obj5 = (SomeObject) ois.readObject();
ois.close();
System.out.println("Deserialized object: " + obj5);
// 注意:这里也没有调用构造函数!
System.out.println("obj1 == obj5 ? " + (obj1 == obj5));
// false,是不同的对象实例
}
}

输出示例:

--- 1. Using new keyword ---
SomeObject(value, id) constructor called.
SomeObject [value=New Object, id=1]
--- 2. Using Class.newInstance() (Deprecated in Java 9+) ---
SomeObject() constructor called.
SomeObject [value=default, id=0]
--- 3. Using Constructor.newInstance() (Reflection) ---
SomeObject(value, id) constructor called.
SomeObject [value=Reflected Object, id=3]
--- 4. Using Object.clone() ---
Cloned object before modify: SomeObject [value=New Object, id=1]
Original object after modify cloned one: SomeObject [value=New Object, id=1]
Cloned object after modify: SomeObject [value=Cloned Object, id=1]
--- 5. Using Serialization/Deserialization ---
Deserialized object: SomeObject [value=New Object, id=1]
obj1 == obj5 ? false

说明:
你可以观察到,clone() 和反序列化时, SomeObject() constructor called.SomeObject(value, id) constructor called. 都没有打印,这证明它们确实绕过了构造函数的执行。


性能考量与最佳实践

  • 避免不必要的对象创建:
    • 尤其是循环内部,频繁创建小对象会增加GC压力。可以考虑对象池、单例模式或复用现有对象。
    • 字符串拼接:优先使用 StringBuilderStringBuffer,而不是 + 操作符(在循环中)。
    • 使用基本数据类型数组而不是对象数组,如果只需要存储原始数据。
  • 字符串优化:
    • String 是不可变的,每次修改都会创建新对象。
    • String 字面量会进行字符串常量池(String Pool)优化,重复使用现有字符串。
    • 对于拼接操作,使用 StringBuilderStringBuffer
  • 大对象与GC:
    • 创建过大的对象可能直接进入老年代(取决于 PretenureSizeThreshold 参数),减少GC回收效率。
    • 合理设计对象大小,避免单个对象过大。
  • TLAB优化:
    • TLAB的默认大小通常是合适的,但如果应用创建大量小对象,可以考虑调整 TLABSize 参数,但通常不建议手动调整,让JVM自动适应。
  • 对象生命周期管理:
    • 确保不再使用的对象能够及时被GC,避免内存泄漏。
    • 对于长生命周期对象,避免持有短生命周期对象的引用。
  • 设计模式应用:
    • 单例模式 (Singleton): 确保一个类只有一个实例,并提供一个全局访问点。
    • 工厂模式 (Factory Method/Abstract Factory): 封装对象创建的复杂性。
    • 建造者模式 (Builder): 用于创建复杂对象,通过一步步构建来生成最终对象。
    • 原型模式 (Prototype): 通过克隆现有对象来创建新对象,避免重复执行构建过程。

总结

JVM 对象创建是一个精细而复杂的过程,从类加载检查,到内存分配、零值初始化、对象头设置,再到最终的构造函数执行,每一步都环环相扣,共同完成一个新对象的诞生。深入理解这些机制有助于我们更好地编写高效、健壮的Java代码,并在遇到性能问题时能更准确地定位和解决。


posted @ 2025-09-12 10:16  yjbjingcha  阅读(11)  评论(0)    收藏  举报