对象内存布局篇
创建对象的方式
new
的方式创建对象/ Xxx 的静态方法/ XxxBuilder XxxFactory 的静态方法Class
的newInstance()
反射的方式创建对象,只能调用空参的构造器,权限必须是public
, jdk9 及以后过期Constructor
的newInstance(Xxx)
反射的方式,可以调用空参、带参的构造器,权限没有要求,实用性更强, jdk9 以后出现clone()
不调用任何构造器,当前类需要实现Cloneable
接口,实现clone()
,默认浅拷贝 (引用类型属性不拷贝)- 使用反序列化从文件、数据流中转换为对象
- 第三方库
Objenesis
,利用asm
字节码技术动态生成构造器,调用构造器创建对象
创建对象的步骤
从字节码角度看对象创建过程
- NEW : 如果找不到
Class
对象,则进行类加载。加载成功后,则在堆中分配内存,从Object
开始到本类路径上的所有属性值都要分配内存。 分配完毕之后,进行零值初始化。在分配过程中,注意引用是占据存储空间的,它是一个变量,占据4个字节。这个指令完毕后,将指向对象实例的引用变量压入虚拟机栈顶。 - DUP : 在栈顶复制该引用变量,这时的栈顶有两个指向堆内实例对象的引用变量。如果
<init>
方法有参数,还需要把参数压入操作栈中。两个引用变量的目的不同, 其中压入底下的引用用于赋值,或者保存到局部变量表中,另一个栈顶的引用变量作为句柄调用相关方法。 - INVOKESPECIAL : 调用构造器,需要一个句柄作为参数,句柄就是栈顶的引用变量。
<clinit>
是类初始化时调用的方法;而 <init>
是对象初始化时调用的方法。
从执行步骤角度看对象创建过程
1、判断对象相应的类是否加载、链接、初始化
虚拟机遇到一条 NEW
指令,首先去检查这个指令的参数能否在 Metaspace
常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化
- 如果没有,双亲委派机制,查找加载相应的
.class
文件 - 如果没有找到文件,则抛出
ClassNotFountException
异常 - 如果找到,则进行类加载,并生成对应的
Class
类对象
2、为对象分配内存
- 指针碰撞 : 对于规整内存,使用指针碰撞算法,分配内存
- 空闲列表 : 对于非规整内存,使用空闲列表算法,分配内存
注意:分配内存时,虚拟机需要考虑处理并发安全问题
3、初始化分配到的内存空间
JVM 将分配到的内存空间进行零值初始化(不包括对象头),这一操作保证对象的字段不用赋值程序即可访问
4、设置对象的对象头
5、执行 <init>
方法进行初始化
对象的内存布局
- 对象头 (Header)
- 实例数据 (Instance Data)
- 对齐填充 (Padding)
对象的访问定位
直接使用指针访问
栈里面的引用指向堆中的对象实例,堆中的对象的对象头指向方法区中的类元数据, HotSpot
选择这种方式定位对象
句柄访问
堆划分出一块内存来作句柄池