Java虚拟机 对象创建流程初始化流程

Posted baiiu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java虚拟机 对象创建流程初始化流程相关的知识,希望对你有一定的参考价值。

前言

梳理对象创建流程和初始化流程

对象创建流程

  1. new指令时,定位该指令的参数在常量池中的符号引用
    • 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,
    • 使用java.lang.reflect包的方法对类进行反射调用时。
    • 当初始化一个类时,发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    • 当虚拟机启动时,虚拟机会先初始化包含main()方法的主类。
    • 被动引用不会触发类的初始化:
      • 子类引用父类的静态字段,不会导致子类初始化。
      • 通过数组定义来引用类,不会触发此类的初始化。new ChildClass[0]。
      • 引用类中的常量,不会触发此类的初始化。
  2. 如果没有,则进行类的加载、连接和初始化。
  3. 虚拟机为新生对象分配内存
  4. 将分配到的内存空间都初始化为零值,不包括对象头,并初始化对象头(哈希码、gc年龄等)
  5. 调用对象<init>方法

另外new对象操作不是原子性的,

  1. 给Singleton实例分配内存
  2. 调用Singleton()构造函数,进行初始化
  3. 将mSingle对象指向分配的内存空间
    因为按照内存模型中线程工作内存回写到主内存时2、3的步骤是不确定的,当3在前面发生时,会引发DCL(Double check Lock)失效问题,可以使用volatile禁止指令重排,保证按顺序执行,直接从主内存读写,就不会出错了。

1. 类加载流程

深入理解Java虚拟机 第七章 虚拟机类加载时机与过程

2. 对象的内存布局

对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

1. 对象头

Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针),数组会多1字宽(32位: 4字节)来存储数组长度。

  • Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
  • Klass Point是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;

2. 实例数据

该部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,包括父类继承下来的和子类中定义的。

3. 对齐填充

该部分不是必须的,某些虚拟机要求对象的起始地址必须是8字节的整数倍,即对象大小必须是8字节的整数倍,所以不足时就需要对齐。

4. Mark Word详解

JVM中对象头的方式有以下两种(以32位JVM为例):

// 普通对象
|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

// 数组对象
|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|
  • Mark Word
    这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
    mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。
    Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:
|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:0 |lock:01 |     Normal无锁      |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1| lock:01 |     Biased偏向锁    |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30         | lock:00 | Lightweight Locked轻量级锁 |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30 | lock:10 | Heavyweight Locked重量级锁 |
|-------------------------------------------------------|--------------------|
|                                             | lock:11 |    Marked for GC   GC标记|
|-------------------------------------------------------|--------------------|

  • lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
  • biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
  • age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
  • identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
  • thread:持有偏向锁的线程ID。
  • epoch:偏向时间戳。
  • ptr_to_lock_record:指向栈中锁记录的指针。
  • ptr_to_heavyweight_monitor:指向管程Monitor的指针。

3. 对象初始化顺序

根据类加载流程,先执行类初始化 clinit,再执行对象初始化init方法。顺序如下:

  1. 父类静态变量
    子类静态变量

  2. 父类静态代码块
    子类静态代码块

  3. 父类方法块
    父类构造函数

  4. 子类方法块
    子类构造函数

3. 对象的访问定位

目前有两种主流方式访问:

  • 句柄
    需要堆单独划出一块内存来作为句柄池,但好处是reference中存储的是稳定的句柄地址,在对象因GC移动时只会改变句柄中的实例指针。

  • 直接指针
    好处是速度快

由此知道在GC Roots里,虚拟机栈(栈帧中的本地变量表)中引用的对象指的是什么了。

结语

本文梳理了对象创建的大概流程,对理解虚拟机大有帮助。

以上是关于Java虚拟机 对象创建流程初始化流程的主要内容,如果未能解决你的问题,请参考以下文章

Android Dalvik虚拟机 堆初始化流程

Android ART虚拟机 对象创建内存分配流程

Android Dalvik虚拟机 对象创建内存分配流程

Android Dalvik虚拟机 对象创建内存分配流程

设计模式代理模式 ( 动态代理 | 模拟 Java 虚拟机生成对应的 代理对象 类 )

虚拟机(JVM)如何加载类