Follow me on GitHub

对象创建时发生了什么?

HostSpot 虚拟机 + 普通 Java 对象,不包括数组、Class 对象。

Java 中对象创建的主要场景:

  1. new 关键字
  2. 反射
    1. 只能通过 Class.newInstance() 调用 无参构造方法,如果要调用其他构造方法,需要用 Class.getConstructor(参数列表) 先获取构造方法,然后再调用;
    2. Class.newInstance() 返回类型为 Object,需要强制类型转换;
  3. 反序列化
  4. 克隆(clone()

按照是否借助构造方法,可以分为两类:

  • new、反射会直接 or 间接使用构造方法;
  • 不借助
    • 反序列化直接从二进制字节流构造对象;
    • clone() 是 native 方法,属于内置机制;

对于最常见的 new 关键字这种方式而言,一般涉及两条 JVM 指令:

  1. new 指令;
  2. invokespecial 指令,执行 <init> 进行对象初始化;

这里的 new 指 JVM 支持的字节码指令,而非 Java 语言层面的 new 关键字。

JVM 维度

创建对象在 JVM 维度体现为对 new 指令的执行过程,根据 JVM 规范,new 指令 格式如下:

new indexbyte1 indexbyte2

其中 indexbyte1indexbyte2 用于计算一个 index,该 index 指向 当前类 的 run-time constant pool,并且 JVM 规定该位置的常量必须是一个代表类 or 接口的符号引用。

1. 类加载

当 JVM 遇到一个 new 指令时,处理如下:

  1. 根据 new 指令的参数,定位到 运行时常量池 中某个代表类 or 接口的 符号引用
  2. 若该符号引用未被加载、解析、初始化,则执行该类的 类加载

2. 分配内存

类加载完成后,JVM 为新生对象分配内存,由于对象所需 内存大小类加载 阶段便可完全确定(原因在《对象的内存布局》解释),因此内存分配任务只需将 确定大小的内存 中划分出来,留给新生对象即可。

根据堆内存是否规整,内存分配算法分为两类:

  1. 指针碰撞
  2. 空闲列表

堆内存是否规整由垃圾收集器的回收算法是否带有 压缩整理 功能决定,因此根据 JVM 运行时使用的垃圾收集器不同,JVM 会采用不同的内存分配方式:

  • 指针碰撞:Serial/ParNew 等带有 压缩 功能的收集器;
  • 空闲列表:CMS 等使用 标记-清除(Mark-Sweep)算法的收集器;

指针碰撞

若堆内存”整整齐齐“,所有已用内存在一边,所有空闲内存在另一边,中间分界点有一个指针作为指示器,则内存分配只需将指针向空闲内存一侧移动对象所需内存大小即可。

空闲列表

若堆内存不规整,已用内存、空闲内存互相交错,则无法使用指针碰撞,JVM 必须动态维护一个可用内存块的列表,每次为对象分配内存时,就从表中找一块大小合适的空闲内存给该对象,并更新列表。

3. 内存初始化

内存分配完成后,JVM 会将这块内存空间除 对象头 以外,全部初始化为 零值

初始化为零值是最基本的初始化,可以保证对象的 实例字段 即使没有显式赋初始值也能直接使用,只不过都是各个类型的零值:

  1. boolean 零值为 false
  2. 引用零值为 null
  3. 数值类型零值为 0;

4. 设置对象头

对象头分为 3 部分:

  1. Mark Word
  2. 类型指针
  3. 数组长度

JVM 会为每个部分设置合理的值,例如:

  1. 根据 JVM 是否允许偏向,设置偏向锁标志;
  2. 将该对象所属类型的指针写入类型指针,这样才能知道该对象是哪个类的实例。

Java 语言维度

到此为止,从 JVM 维度看,一个新对象已经创建完成,但从 Java 语言维度看,该对象的创建才刚刚开始,毕竟构造函数都还没开始执行。

5. 执行 init 方法

对象创建在 Java 语言维度,仅涉及对象初始化,即:

  • 构造方法
  • 字段初始化
  • {}

它们被 JVM 归一到 <init> 方法中,因此对象创建的最后一步为通过 invokespecial 调用 <init> 方法,完成程序员对对象初始化做出的规定。