hotspot虚拟机对象创建

对象创建

类加载检查

虚拟机遇到一条new指令时,首先去检查这个指令号的参数是否能在常量池中定位到这个符号的引用,并且检查这个符号引用代表的类是否已被加加载,连接,初始化过如果没有,那必须先执行相应的类加载流程

分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存带下在类加载完成后便可以确定,为对象分配空间的任务等同于把一块确定带下的内存从java堆中划分出来。分配的方式有指针碰撞,和空闲列表俩种,选择哪种方式分配由java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

分配的俩种方式

指针碰撞

使用场景:规整的该内存(没有内存碎片)的情况下

用过的内存全部整合到另一边,没用过的内存放放在另一边,中间有一个分解指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可

使用该分配方式的GC收集器Serial,ParNew

空闲列表

适用场合:内存不规整的情况下。

原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块足够大的内存块来分给对象实例,最后更新列表记录

使用该分配方式的GC收集器CMS

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

初始化零值

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

设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,列入对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码码,对象的GC分代年龄信息等。这些存放在对象头中,另外虚拟机根据当前运行状态的不同,对象头会有不同的设置方式

执行init方法

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

对象

java 对象介绍

对象的实例化

方式

对象实例化的方式有如下

  • new创建对象
  • newInstance()反射创建
  • close
  • 反序列化创建
  • 第三方类库

创建步骤

  • 盘对对象对应的类是否加载,链接,初始化

虚拟机遇到一条new指令,首先去检查这个指令的参数能否在metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化.(即判断元信息是否存在).如果没有,那么在双亲委派模式下,使用当前类加载器classloader+包名+类名为key进行查找对应的class文件,则抛出classnotfountExeption异常,如果找到,则进行类加载,并生成对应的class类对象

  • 为对象分配内存

首先对计算对象占用空间大小,接着在堆中划分一块内存给对象.如果实例成员变量时引用变量,仅分配阴影变量空间即可,即四个字节大小

如果内存规整,那么虚拟机采用的是指针碰撞来为对象分配内存.意思是所有用过的内存放在一边,空闲的内存存放在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲的那边挪动一段对象大小相等的距离罢了.如果垃圾收集器选择的是Serial,parNew这种基于压缩算法的,虚拟机采用这种分配方式.一般带有compact(整理)过程的收集器时,使用指针碰撞

如果内存是不规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存.

意思是虚拟机维护了一个列表,记录哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容.这种分配方式也被称为空闲列表(free list)

至于选择哪种方式由java堆是否规整决定,而java堆是否规整又由锁采用的垃圾收集器是否带有压缩整理功能决定

  • 初始化分配到的空间

设置所有属性默认值,保证对象实例字段在不赋值时可以直接使用

  • 设置对象头

将对象的所属类(即类的元数据信息),对象的hashcode和对象的gc信息,锁信息等数据存储在对象头中.这个过程的具体设置方式取决于jvm实现

  • 执行init方法进行初始化

在java程序视角看来,初始化才正式开始,.初始化成员变量,执行实例化代码块,调用类的构造方法,吧堆内对象的首地址复制给引用变量

因此一般来说(由字节码中是否1跟随者invokespecial指令所决定),new指令之后会接着就是执行方法,把对象按照程序员的意向初始化,这样一个真正可用的对象才算完全创建出来.

对象的内存布局

对象头

运行时元数据(mark word)

  • 哈希值(HashCode)

返回该对象的哈希码值。支持此方法是为了提高哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。

在java应用程序执行期间,在同一对象多次调用hashCode方法的时,必须一致的返回相同的整数,前提是将对象进行equlas比较时所用的信息没有更改,从某一应用程序的一次执行到图片那个一应用程序的另一次执行,该整数无需保持一致

  • GC分代年龄
  • 锁状态的标志
  • 线程持有的锁
  • 偏向线程ID
  • 偏向时间戳

类型指针

指向元数据InstanceKlass,确定该对象所属的类型

说明:如果创建的是数组还需要记录数组长度

实例数据

它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类集成下来的和本身拥有的字段)

规则

  • 相同宽度的字段总是会被分配到一起
  • 父类中定义的变量会出现在子类之前
  • 如果compactFields参数为true:子类窄变量可能插入到父类变量的空隙

对齐填充

不是必须的没有特别含义,仅仅起到占位符作用

NnCk5t.md.png

对象的访问定位

jvm是如何通过栈帧中的对象引用访问到其内部的对象实例呢?

定位通过站上引用访问

对象访问的方式主要有俩种

句柄访问

栈的本地变量表中记录了对象引用,在堆空间开辟了一块句柄池,一个对象对应着一个句柄.句柄有俩个信息,一个是到对象实例数据的指针,指向了堆中的实例数据.一个是到对象类型数据的指针,指向了方法区中的对象类型数据.

会浪费一些空间,效率比较低.

引用中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改动句柄中实例数据指针即可,reference本身不需要被修改

直接指针

hotspot采用

栈空间的引用只想堆空间对象实体,对象实体有类型指针指向方法区中的类元数据.

Last modification:January 13, 2023
如果觉得我的文章对你有用,请随意赞赏