Skip to content

对象内存布局

字数: 0 字 时长: 0 分钟

创建对象的方式

1. 使用 new 关键字

直接使用 new 调用构造函数、或者建造者模式、方法引用等方式本质都是通过构造函数创建对象

2. 反射机制

利用反射机制创建对象(本质还是需要调用构造函数),好处是可动态创建对象,坏处是性能相对直接 new 较低

java
// Class.newInstance()(JDK9 已废弃)
Person person1 = Person.class.newInstance();

// Constructor.newInstance()(推荐)
Constructor<Person> constructor = Person.class.getConstructor();
Person person2 = constructor.newInstance();

3. clone()

使用 clone() 方法创建对象,前提是必须实现 Cloneable 接口,默认浅克隆,不调用构造函数

4. 反序列化

从字节流重建对象,类必须实现 Serializable 接口,不调用构造函数,性能最低(比反射还低得多)

5. Unsafe 类

Unsafe 类可以绕过构造函数,底层直接分配内存,仅用于特殊场景(如序列化库),常规开发禁用

java
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

// 直接分配内存(不调用构造函数)
Person p = (Person) unsafe.allocateInstance(Person.class);

创建对象的步骤

从字节码角度看对象创建过程

对象创建过程.webp

  1. NEW:如果找不到 Class 对象,则进行类加载。加载成功后,则在堆中分配内存,然后进行零值初始化。在分配过程中,注意引用是占据存储空间的,它是一个变量,占据4个字节。这个指令完毕后,将指向对象实例的引用变量压入虚拟机栈顶。
  2. DUP:在栈顶复制该引用变量,这时的栈顶有两个指向堆内实例对象的引用变量。如果 <init> 方法有参数,还需要把参数压入操作栈中。两个引用变量的目的不同,底下的引用用于赋值,或者保存到局部变量表中,另一个栈顶的引用变量作为句柄调用相关方法。
  3. INVOKESPECIAL:调用构造器,需要一个句柄作为参数,句柄就是栈顶的引用变量。

从执行步骤角度看对象创建过程

1. 判断对象相应的类是否加载、链接、初始化

虚拟机遇到一条 NEW 指令,首先去检查这个指令的参数能否在 Metaspace 常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化

  • 如果没有,双亲委派机制,查找加载相应的 .class 文件
  • 如果没有找到文件,则抛出 ClassNotFountException 异常
  • 如果找到,则进行类加载,并生成对应的 Class 类对象

2. 为对象分配内存

  • 指针碰撞 : 对于规整内存,使用指针碰撞算法,分配内存

规整内存.webp

  • 空闲列表 : 对于非规整内存,使用空闲列表算法,分配内存

非规整内存.webp

3. 初始化分配到的内存空间

JVM 将分配到的内存空间进行零值初始化(不包括对象头),这一操作保证对象的字段不用赋值程序即可访问

4. 设置对象的对象头

5. 执行 <init> 方法进行初始化

对象的内存布局

  • 对象头 (Header)
  • 实例数据 (Instance Data)
  • 对齐填充 (Padding)

对象的内存结构.webp

对象的访问定位

里面的引用指向中的对象实例,堆中的对象的对象头指向方法区中的类元数据

指针访问对象.webp