当前位置:   article > 正文

JVM面试题详解系列——Java 对象的创建过程_java对象的创建过程

java对象的创建过程

Java对象创建的过程

Java对象创建的过程主要分为五个步骤,下面我将详细介绍这五个步骤。

Step1:类加载检查

虚拟机遇到一条new指令时,首先会去检查这个指令的参数是否能在class文件中的常量池中定位到这个类的符号引用,并且会检查这个符号引用所指向的类是否已经完成加载、连接和初始化,如果没有,必须先执行相应类的类加载过程。

关于类加载过程详细解读,可以参考这篇文章——JVM面试题详解系列——类加载过程详解

Step2:分配内存

当类加载检查通过后,虚拟机会为新生对象分配内存空间,对象所需内存空间的大小在类加载完成后就已经确定了。为新生对象分配内存空间其实就是在Java堆中划分出一块确定大小的内存分配给新生对象。分配内存的方式有“指针碰撞”和“空闲列表”两种,选择哪种分配方式取决于Java堆内存是否规整。

内存分配的两种方式
指针碰撞
  • 使用场合:堆内存规整(即没有内存碎片)的情况下。
  • 实现原理:将用过的内存都整合到一边,没有用过的内存放到另一边,中间有一个分界指针,当需要为新对象分配内存空间时,只需要将分界指针向没有用过的内存一侧移动对象内存大小位置即可。
空闲列表
  • 使用场合:堆内存不规整的情况下。
  • 实现原理:虚拟机会维护一个列表,该列表记录了哪些内存是可用的,当需要为新对象分配内存空间时,只需要在列表中找一块足够大小的内存分配给对象实例,然后更新列表记录。

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器垃圾采用的垃圾收集算法,垃圾收集相关内容我会在后续文章详细介绍。

如果想了解垃圾回收相关的内容,可以参考我的另一篇文章——JVM面试题详解系列——垃圾回收详解。

Step3:初始化零值

内存分配完成后,虚拟机需要将新分配的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段可以在Java代码中可以不赋初始值就直接使用,程序能够访问这些实例字段的数据类型所对应的零值。

例如,如果一个类定义了一个int类型的实例字段,并且没有给它赋初值,那么在创建该类的新实例时,该字段的值就会被初始化为0。这是因为,Java虚拟机在为对象分配内存后,会将分配的内存空间清零,这就保证了实例字段的默认值都为0。在Java中,每种数据类型都有一种默认值,例如int类型的默认值为0,boolean类型的默认值为false,引用类型的默认值为null等。因此,程序能够访问这些实例字段的数据类型所对应的零值,也就是默认值。这些默认值通常是Java程序中常见的特殊值,因此程序可以直接使用它们,而不需要进行赋初值的操作。

Step4:设置对象头(Kclass Point 和 Mark Word)

初始化零值之后,虚拟机需要对对象头进行必要的设置,例如这个对象是哪个类的实例,如何才能找到这个类的元数据信息,对象的哈希码、GC 分代年龄、锁标志位、偏向锁标志位、线程持有的锁信息、对象是否可用等信息会存放到对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

如果想要详细了解对象头相关信息,可以参考我的另一篇文章——Java锁机制详解。

Step5:执行init方法

执行完上面四个步骤后,从虚拟机的角度来看,一个新对象已经产生了,但是从Java程序的角度来看,对象的创建才刚刚开始,init()方法还没有执行,所有的字段都还是零值,所以,一般来说,执行完new指令后会接着执行init方法,将对象按照程序员的需求来进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的内存布局

在 Hotspot 虚拟机中,对象在堆内存中的布局可以分为 3 块区域:对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。

在这里插入图片描述

对象头

对象头由两部分组成:对象标记Mark Word和类元信息(又叫类型指针)组成。
在这里插入图片描述

对象标记(Mark Word)

对象标记用于存储对象自身的运行时数据,例如对象的哈希码、GC 分代年龄、锁标志位、偏向锁标志位、线程持有的锁信息、对象是否可用等信息,这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。例如当对象被锁定时,Mark Word会存储锁的信息;当对象被设置了偏向锁时,Mark Word会存储偏向锁的信息。这些信息对于Java虚拟机的运行和垃圾回收非常重要。

对象标记通常包括以下几个部分:

  1. 对象的哈希码:用于快速比较对象是否相等。

  2. GC分代年龄:标记对象在年轻代或老年代中的年龄。

  3. 锁标志位:用于标记对象是否被锁定,以及锁定的类型。

  4. 偏向锁标志位:用于标记对象是否被设置了偏向锁。

  5. 线程持有的锁信息:用于记录当前线程持有的锁的情况。

  6. 对象是否可用:用于标记对象是否已经被回收。

类型指针(Kclass Point)

类型指针是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。

实例数据

实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容,包括从父类继承下来的和本身拥有的字段。

对齐填充

对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针。

句柄

如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。

在这里插入图片描述

直接指针

如果使用直接指针访问,reference 中存储的直接就是对象的地址。
在这里插入图片描述

两种访问方式比较

这两种对象访问方式各有优势,使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改;使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销,HotSpot 虚拟机主要使用的就是这种方式来进行对象访问。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/一键难忘520/article/detail/813251
推荐阅读
相关标签
  

闽ICP备14008679号