java对象创建流程

对象创建流程

推荐博客

创建触发

  关于对象的创建一般是从new指令(我说的是JVM的层面)开始的。 虚拟机遇到一条new指令时,会先去检查这个指令的参数能否在方法区中的常量池中检索到一个类的符号应用,并且检查这个符号引用代表的类是否已被加载、解析、初始化。如果没有,则必须先执行相应的类加载过程。(下次会介绍类的加载过程)。

分配内存

类加载检查通过后,接下来JVM开始为对象在堆中分配内存对象所需要的内存大小在类加载完成后遍可以完全确定。为对象分配空间的任务就相当于把一块确认大小的内存从java堆中划分出来。而根据内存是否规则化分为两种情况:

  • “指针碰撞方式”:java堆中的内存是绝对规整的。所有用过的内存都放在一边,所有未使用过的内存放在另一边。中间放着一个指针作为分界点的指示器。那在这种情况下分配内存就相当于指针往空闲方向移动了一段与对象大小相等的距离。
  • “空闲列表方式”:java堆中的内存不是规整的,已使用内存和空闲内存相互交错,这时候就不能简单的像指针碰撞一样为对象分配内存了,JVM就需要维护一个列表,记录哪些内存是使用过的,哪些是空闲的,在为对象分配内存的时候从这个列表中找到一个足够大的空间划分给对象,并更新列表上的记录。

  **因此,选择哪种分配方式实际上是由内存是否规整来决定,而内存是否规整又由JVM选择的垃圾收集器是否带有压缩整理内存功能的有关系。**比如serial、ParNew等使用复制/标记整理的垃圾收集器,就是使用指针碰撞方式。而像CMS这种基于Mark-Sweep(标记-整理)算法的收集器,则JVM使用空闲列表方式分配内存。关于垃圾回收器的知识,我会在后面单独讲。

  同时,除了要考虑内存分配的方式之外,还需要考虑内存分配的并发性,要保证指针的一致性。因为对象在JVM中的创建是非常频繁的行为,即使是仅仅修改一个指针所指向的位置就能为对象分配内存,在并发的情况下也不是线程安全的。可能出现正在给对象A分配内存,指针还没来得及修改,JVM又使用原来的指针为对象B分配了内存。解决这个问题有两种方法:

  1. 对分配内存空间的动作进行同步处理——实际上虚拟机采用了CAS配上失败重试的方式保证更新操作的原子性。
  2. 另一种方式是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一块内存,称为本地线程分配缓存(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在该线程对应的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要锁定同步。虚拟机是否使用TLAB,可以通过==-XX:+/-UseTLAB==参数来指定。

对象的内存布局

  在HotSpot虚拟机中,对象在内存中存储地布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。其中要注意的是,实例字段包括自身定义的和从父类继承下来的(即使父类的实例字段被子类覆盖或者被private修饰,都照样为其分配内存)。相信很多人在刚接触面向对象语言时,总把继承看成简单的“复制”,这其实是完全错误的。JAVA中的继承仅仅是类之间的一种逻辑关系(具体如何保存记录这种逻辑关系,则设计到Class文件格式的知识,具体请看我的另一篇博文),唯有创建对象时的实例字段,可以简单的看成“复制”。

  如果对象是数组类型,那么JVM将会用3个字宽度存储对象头,如果是非数组类型,则用2字宽存储对象头。在32位的虚拟机里面,1字宽是4字节。

长度 内容 说明
32/64Bit Mark Word 存储对象的hashCode或锁信息
32/64Bit Class Metadata Address 存储到对象类型数据的指针
32/64Bit Array length 数组的长度(如果对象是数组)

  Java对象的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位的JVM的Mark Word的默认存储结构如下所示:

锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位
无锁状态 对象的hashcode 对象分代年龄 0 01

  在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据,如下所示:

锁状态 25bit 4bit 1bit 2bit
23bit 2bit 是否是偏向锁 锁标志位
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10
GC标记 11
偏向锁 线程ID Epoch 对象分代年龄 1 01

初始化

  内存分配完成后,虚拟机需要将分配到的内存空间初始化零值(不包括对象头),如果使用的是TLAB,这一工作过程也可以提前至TLAB分配时进行。这一过程保证了实例字段在java代码中可以不赋值就直接使用,程序能访问到这些字段的数据类型所对应的零值。而方法的局部变量却必须要显示初始化后才可以访问。

  接下来,JVM要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁,对象头会有不同的设置方式。如前面所讲。

执行–init方法

  在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从java程序的视角来看,对象创建才刚刚开始——init方法还没有执行,所有的字段都还为零。所以,一般来说,执行new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,如:int a=7;这样一个真正可用的对象才算完全产生出来。

执行构造函数

  执行相应的构造函数

你可能感兴趣的:(java学习笔记)