【JVM】详解 对象的创建

目录

创建对象的方式

new关键字

反射

Class类的newInstance()方法

Constructor类的newInstance()方法

clone()

反序列化

创建对象的流程

类加载检查

内存分配

[指针碰撞(Bump The Pointer)](#指针碰撞(Bump The Pointer))

[空闲列表(Free List)](#空闲列表(Free List))

并发问题

初始化零值

对象头设置

方法

总结


创建对象的方式

new关键字

通过 new 关键字调用类的构造方法创建对象,是最直接、最常见的方式。

java 复制代码
public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
}

// 创建对象
Person person = new Person("Alice");

反射

java 复制代码
// 方式1:通过 Class 类的 newInstance()(已过时,推荐用 Constructor)
Class<Person> clazz = Person.class;
Person person1 = clazz.newInstance(); // 调用无参构造

// 方式2:通过 Constructor 类(支持有参/私有构造)
Constructor<Person> constructor = clazz.getConstructor(String.class);
Person person2 = constructor.newInstance("Bob");

Class类的newInstance()方法

  • 只能调用类的无参构造方法 ,且要求该构造方法必须是可访问的 (非 private)。
  • 若类没有无参构造方法,或无参构造方法是私有的,调用时会直接抛出 InstantiationExceptionIllegalAccessException

Constructor类的newInstance()方法

  • 支持调用类的任意构造方法(包括无参、有参、甚至私有构造方法)。
  • 只需通过 Class.getConstructor(参数类型...)Class.getDeclaredConstructor(参数类型...) 获取对应构造器,再传入参数即可。
  • 对于私有构造方法,可通过 Constructor.setAccessible(true) 突破访问权限限制。

clone()

通过对象的 clone() 方法创建其副本,无需调用构造方法,属于浅拷贝(默认)。

java 复制代码
public class Person implements Cloneable {
    private String name;
    // 重写 clone() 方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

// 创建对象副本
Person original = new Person("Charlie");
Person clone = (Person) original.clone();

反序列化

将序列化(如通过 ObjectOutputStream 写入磁盘)的对象字节流恢复为内存对象,无需调用构造方法。

java 复制代码
import java.io.*;

public class Person implements Serializable {
    private String name;
    // 序列化
    public static void serialize(Person p) throws IOException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.obj"))) {
            oos.writeObject(p);
        }
    }
    // 反序列化(创建对象)
    public static Person deserialize() throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.obj"))) {
            return (Person) ois.readObject();
        }
    }
}

// 使用
Person original = new Person("David");
Person.deserialize(); // 从文件恢复对象

创建对象的流程

创建对象通常都是用 new 关键字创建,但在虚拟机中远比表面复杂。接下来将对对象创建流程作详细解析。

类加载检查

当 JVM 检测到字节码 new指令 时,会先对其进行类加载检查 。查看指令参数在常量池中是否对应一个类的符号引用 ,并检查这个类的符号引用有没有经历过类加载的流程。如果没有,那必须先执行相应的类加载过程

内存分配

类加载通过后,对应类的对象大小也被确定下来。接下来将要从堆中划分出一定大小的空间块用来存储类的实例。

指针碰撞(Bump The Pointer)

假设堆是规整的,不存在空间碎片,那么可以直接通过指针碰撞来进行对象空间的分配。

所谓指针碰撞,就是将堆顶上移一定的空间,用来存储类的实例。

空闲列表(Free List)

但当堆空间是不规整的,已被使用的内存和空闲的内存相互交错在一起,即存在空间碎片,那么必须维护一个空闲列表,其中存放着可用的内存块并记录其大小。当新的对象实例请求分配空间时,就会访问空闲列表然后找到足够大的空闲内存块。

并发问题

对象创建在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选方案:

  • 分配空间时通过CAS + 失败重试保证分配空间的原子性,直到分配到可用空间。
  • 通过不同线程对应的 TLAB 来分配不同线程创建的对象。哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。

初始化零值

为新对象分配完一块连续的内存空间后,会自动将这块内存中除了对象头(Object Header)之外的所有字节都初始化为零值 (如 00Lnullfalse 等,对应不同数据类型的默认值)。因此对象的实例字段可以不赋初始值就直接使用。

如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。

对象头设置

接下来对对象头做必要的设置。例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)​、对象的GC分代年龄等信息。

<init>方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是对象实例的字段值依然为0,依次还要执行构造方法 :虚拟机调用对象的<init>()方法(即构造函数的字节码表现形式),执行开发者定义的初始化逻辑 ------ 包括显式字段赋值、代码块执行、父类构造函数调用等。这一步将对象从 "零值状态" 调整为 "业务预期状态"。

这样一个真正可用的对象才算完全被构造出来。

总结

相关推荐
weixin_445476683 小时前
Java并发编程——提前聊一聊CompletableFuture和相关业务场景
java·并发·异步
ChinaRainbowSea3 小时前
11. Spring AI + ELT
java·人工智能·后端·spring·ai编程
不会写DN3 小时前
用户头像文件存储功能是如何实现的?
java·linux·后端·golang·node.js·github
聪明的笨猪猪4 小时前
Java JVM “类加载与虚拟机执行” 面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
盖世英雄酱581364 小时前
FullGC排查,居然是它!
java·后端
老K的Java兵器库4 小时前
集合性能基准测试报告:ArrayList vs LinkedList、HashMap vs TreeMap、并发 Map 四兄弟
java·开发语言
Knight_AL4 小时前
如何解决 Jacob 与 Tomcat 类加载问题:深入分析 Tomcat 类加载机制与 JVM 双亲委派机制
java·jvm·tomcat
哲学七4 小时前
Springboot3.5.x版本引入javaCv相关库版本问题以及精简引入包
java·ffmpeg