09. new一个对象,JVM到底做了什么

101 阅读5分钟

JVM创建对象的流程如下:

640.webp

JVM如何创建一个对象

1. 类加载检查

  • 类加载:首先,JVM检查当前要创建的对象所属的类是否已经被加载。如果该类尚未加载,则会触发类加载机制,将该类的字节码加载进内存,并初始化类的相关信息。
  • 检查加载:在类加载之后,JVM会检查常量池中的符号引用,确认该类的元数据信息是否可用,并进行解析。解析是将符号引用(如类名)解析为具体的内存地址。

2. 内存分配

  • 指针碰撞:如果堆内存是规整的,那么JVM只需移动一个指针,将该指针移动到空闲内存的一部分,从而分配给新对象。
  • 空闲列表:如果堆内存不规整,JVM会通过维护一个空闲内存块的列表,从中挑选出足够大的内存块分配给对象。这种方式的效率比指针碰撞低一些。

并发安全问题:

  • CAS与失败重试:为了保证分配内存的并发安全,JVM使用了CAS(Compare-And-Swap)操作,配合失败重试机制,确保多线程环境下的内存分配是原子操作。
  • TLAB(线程本地分配缓冲):JVM可以为每个线程分配一个小的缓冲区,叫做线程本地分配缓冲区。线程优先从自己的TLAB中分配内存,从而减少了同步的开销,提高了并发分配的效率。

3. 内存空间初始化

在分配了内存之后,JVM会将该内存区域初始化为零值。这一步骤确保了对象的每个字段都有一个默认的初始值,通常是对应数据类型的零值(如int类型为0,boolean类型为false)。

4. 设置对象头

对象头包含了对象的元数据信息。JVM在分配好对象后,会在对象头中存储一些基本信息,包括:

  • 对象的类的元数据信息(如类类型指针)
  • 对象的哈希码
  • 对象的GC代龄(垃圾回收使用的代龄信息)

5. 对象初始化

分配和设置完成后,JVM调用对象的构造函数进行初始化,这一步是按照程序员的逻辑初始化对象的各个字段,将其赋予实际的初始值。主要是对实例数据的初始化

经过以上五个步骤,JVM成功创建了一个对象并准备将其交给应用程序使用。

对象在堆中的布局,参考01. 对象在堆中的布局.md

那么问题来了,如果它还有父类,并且父类未加载进JVM中呢?

带父类时JVM如何创建对象

假如有父类,且父类尚未加载的情况下,加载和初始化流程如下:

1. 检查当前类是否已加载

  • 检查当前类:JVM首先检查当前要创建对象的类是否已经被加载。如果当前类尚未被加载,JVM将触发类加载过程

2. 父类加载

  • 递归加载父类:在加载当前类之前,JVM会递归检查父类(以及父类的父类,直到java.lang.Object)是否已经被加载。
    • 如果父类还没有被加载,则会从父类开始加载,一直递归到最顶层的Object类。
    • 类的加载顺序是:先加载最顶层的父类(Object类)-> 然后是父类 -> 最后是子类。

3. 父类初始化

  • 递归初始化父类:JVM会首先初始化父类。父类初始化的顺序如下:
    1. 静态变量初始化:父类中的静态变量会被赋值。
    2. 静态代码块:如果父类有静态代码块,则会在此阶段执行。
  • 顶级父类java.lang.Object的初始化:所有Java类最终都继承自Object类,因此Object类会是最先被初始化的。

4. 子类加载和初始化

  • 子类的加载和初始化:在父类加载和初始化完成后,JVM会继续加载和初始化子类。

    • 子类的静态变量被赋值。
    • 子类的静态代码块被执行。
  • 注意:即使子类触发了初始化过程,父类的静态变量和静态代码块也会比子类更早执行,因为父类的初始化是先于子类的。

5. 创建对象(实例初始化)

  • 内存分配

    :当父类和子类的类加载和初始化完成后,JVM会为这个新对象分配内存,并初始化实例变量。

    • 首先,父类的实例变量将被初始化为默认值。
    • 然后,执行父类的构造函数,依次初始化父类的实例变量并调用父类的构造器。
    • 接着,子类的实例变量将被初始化为默认值。
    • 最后,执行子类的构造函数,依次初始化子类的实例变量并调用子类的构造器。

其实就是加载父类的类信息,然后进行父类的初始化,然后创建对象时按顺序进行父类和子类的实例变量和构造函数的执行。

这一部分可以详细阅读:06.类加载机制的4:类加载顺序。 以做互相映照