对象内存布局
字数: 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);
创建对象的步骤
从字节码角度看对象创建过程
- NEW:如果找不到
Class
对象,则进行类加载。加载成功后,则在堆中分配内存,然后进行零值初始化。在分配过程中,注意引用是占据存储空间的,它是一个变量,占据4个字节。这个指令完毕后,将指向对象实例的引用变量压入虚拟机栈顶。 - DUP:在栈顶复制该引用变量,这时的栈顶有两个指向堆内实例对象的引用变量。如果
<init>
方法有参数,还需要把参数压入操作栈中。两个引用变量的目的不同,底下的引用用于赋值,或者保存到局部变量表中,另一个栈顶的引用变量作为句柄调用相关方法。 - INVOKESPECIAL:调用构造器,需要一个句柄作为参数,句柄就是栈顶的引用变量。
从执行步骤角度看对象创建过程
1. 判断对象相应的类是否加载、链接、初始化
虚拟机遇到一条 NEW
指令,首先去检查这个指令的参数能否在 Metaspace
常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化
- 如果没有,双亲委派机制,查找加载相应的
.class
文件 - 如果没有找到文件,则抛出
ClassNotFountException
异常 - 如果找到,则进行类加载,并生成对应的
Class
类对象
2. 为对象分配内存
- 指针碰撞 : 对于规整内存,使用指针碰撞算法,分配内存
- 空闲列表 : 对于非规整内存,使用空闲列表算法,分配内存
3. 初始化分配到的内存空间
JVM 将分配到的内存空间进行零值初始化(不包括对象头),这一操作保证对象的字段不用赋值程序即可访问
4. 设置对象的对象头
5. 执行 <init>
方法进行初始化
对象的内存布局
- 对象头 (Header)
- 实例数据 (Instance Data)
- 对齐填充 (Padding)
对象的访问定位
栈里面的引用指向堆中的对象实例,堆中的对象的对象头指向方法区中的类元数据