【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>()方法(即构造函数的字节码表现形式),执行开发者定义的初始化逻辑 ------ 包括显式字段赋值、代码块执行、父类构造函数调用等。这一步将对象从 "零值状态" 调整为 "业务预期状态"。

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

总结

相关推荐
laplace01231 天前
Java八股—MySQL
java·mysql·oracle
熙客1 天前
TiDB:分布式关系型数据库
java·数据库·分布式·tidb
你想考研啊1 天前
linux安装jdk和tomcat和并自启动
java·linux·tomcat
悟能不能悟1 天前
java的java.sql.Date和java.util.Date的区别,应该怎么使用
java·开发语言
高山上有一只小老虎1 天前
java 正则表达式大全
java·正则表达式
_院长大人_1 天前
设计模式-工厂模式
java·开发语言·设计模式
凌波粒1 天前
MyBatis完整教程IDEA版(2)--ResultMap/注解/一对多/多对一/lombok/log4j
java·intellij-idea·mybatis
蓝-萧1 天前
【玩转全栈】----Django基本配置和介绍
java·后端
priority_key1 天前
排序算法:堆排序、快速排序、归并排序
java·后端·算法·排序算法·归并排序·堆排序·快速排序
汤姆yu1 天前
基于SpringBoot的动漫周边商场系统的设计与开发
java·spring boot·后端