Skip to content

对象内存布局篇

创建对象的方式

  • new 的方式创建对象/ Xxx 的静态方法/ XxxBuilder XxxFactory 的静态方法
  • ClassnewInstance() 反射的方式创建对象,只能调用空参的构造器,权限必须是 public, jdk9 及以后过期
  • ConstructornewInstance(Xxx) 反射的方式,可以调用空参、带参的构造器,权限没有要求,实用性更强, jdk9 以后出现
  • clone() 不调用任何构造器,当前类需要实现 Cloneable 接口,实现 clone() ,默认浅拷贝 (引用类型属性不拷贝)
  • 使用反序列化从文件、数据流中转换为对象
  • 第三方库 Objenesis ,利用 asm 字节码技术动态生成构造器,调用构造器创建对象

创建对象的步骤

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

对象创建过程.png

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

<clinit> 是类初始化时调用的方法;而 <init> 是对象初始化时调用的方法。

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

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

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

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

2、为对象分配内存

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

规整内存.png

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

非规整内存.png

注意:分配内存时,虚拟机需要考虑处理并发安全问题

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

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

4、设置对象的对象头

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

对象的内存布局

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

对象的内存结构.png

对象的访问定位

直接使用指针访问

栈里面的引用指向堆中的对象实例,堆中的对象的对象头指向方法区中的类元数据, HotSpot 选择这种方式定位对象

指针访问对象.png

句柄访问

堆划分出一块内存来作句柄池

句柄访问对象.png