JVM 知识 -- 对象初始化过程

本文最后更新于:3 天前

  1. 加载字节码中的文件到内存中的方法区

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

    如果没有,那么先执行相应的类加载过程

  2. 初始化 static 相关的成员变量和静态方法,称为类初始化(全部都在方法区)

    1. 先把所有的 static 部分(成员与方法)申请空间,然后再给每一个 static 成员由上至下分配初始值

    2. 分配完空间之后,会有默认值,而不是指定的值

    3. 这时才执行显式赋值,静态代码块对变量进行赋值,执行顺序与代码一致

  3. 通过 new 在堆内存中申请对象的内存空间,取得首地址

    在类加载检查通过后,要创建的对象所需的内存大小已经确定了,虚拟机将一块确定大小的内存从堆中划分出来分配给对象

    假设堆中所有用过的内存放在一边,空闲的内存放在另外一边,中间放一个指针作为分界点的指示器,那分配内存就是移动这个指针的过程,这种内存分配方式叫"指针碰撞"

    但如果堆中的内存,空闲的和使用过的是交互相错的,不是规整的,那么虚拟机必须维护一个队列,记录哪些内存块是可用的,分配的时候,查找表找到一块足够大的空间划分给对象,并更新表信息,这种方式叫"空闲列表"

    选择哪种分配方式是由堆是否规整决定的,而Java堆是否规则是由所采用的垃圾收集器是否带有压缩整理功能决定的

    • 还需要考虑的一个问题是,在虚拟机中创建对象是否是一个非常频繁的行为

      即便是采用"指针碰撞"的方式,在并发的情况下,修改指针也不是线程安全的

      对于这个问题,有两种解决方法:

      1. 第一,对分配内存空间的操作进行同步处理 – 实际上虚拟机采用 CAS 加自旋的方式保证了更新操作的原子性

      2. 第二,每个线程在堆中预先分配一小块内存,叫本地线程分配缓冲(TLAB)

        哪个线程要分配内存,就在哪个线程的 TLAB 上分配,只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁定

        内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用 TLAB,这一过程也可以提前至 TLAB 分配时进行

        这一操作保证了对象的实例字段在 Java 代码中可以不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值

  4. 接下来,虚拟机要对对象进行必要的设置,例如这个对象属于哪个类,如何才能找到类的元数据信息,对象的哈希码,对象的 GC 分代年龄等信息

到现在为止,从 JVM 的角度来看,一个新的对象已经产生了

但从 Java 程序来看,还需要对其执行 init 方法,所有字段都还是零值

一般来说,执行 new 指令之后会接着执行 init 方法,把对象安照程序员的意愿进行初始化

  1. 构造方法压栈,依次执行以下的操作

    1. 把类的非静态成员变量加载到堆内存中并分配空间同时默认初始化,非静态成员方法加载到方法区中并分配空间,然后由上到下进行非静态成员变量的初始化

    2. 用构造代码块和显示赋值对成员变量进行赋值,执行顺序与代码顺序一致

    3. 执行构造函数

  2. 整个对象初始化完成,返回首地址值

如果有父类,则:

先进行父类静态初始化,然后进行子类静态初始化(加载类文件的先后)

在堆内存空间分配完毕后,然后父类对象初始化,在进行子类对象初始化

(堆内存的创建,在父类对象的基础上加东西变成子类对象)