daicy
发布于 2020-12-31 / 1202 阅读
0
0

JVM之java类对象底层是如何创建的

0、前言

Java程序中 User user = new User();的代码在执行过程中,JVM究竟做了哪些工作?

1、Java类对象的创建过程

Java对象保存在内存中时,主要由三部分组成:对象头、实例数据、对齐填充字段,所以Java对象创建的过程实际上是对这三部分进行配置、补充和初始化的过程。

注:对齐填充字段:在JVM中,要求对象占用内存的大小应该是8bit的倍数,这个信息是用来补齐8bit的,无其他作用

1.1、类加载检查阶段

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

1.2、分配内存

类加载检查通过后,JVM将为新创建对象分配内存。对象所需的内存大小在类加载完成后便可以确定。所以,为对象分配内存空间相当于把确定大小的内存从Java堆中划分出来。分配方式有"指针碰撞"和"空闲列表"两种,选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否具有压缩整理功能所决定(标记-清楚不规整,标记-整理和复制都是规整的)。

1. 指针碰撞

   适用场合:堆内存规整(没有内存碎片)
   原理:用过的内存放一边,没用过的内存放一边,中间有一个分界值指针,分配内存时只需要向着没用过的内存方向将该指针移动对象确定内存大小位置即可
   GC收集器:Serial、ParNew

2. 空闲列表

   适用场合:堆内存不规整
   原理:JVM会维护一个列表,该列表会记录哪些内存块是可用的,分配内存时找一块足够大的内存区域划分给对象实例,最后更新内存列表
   GC收集器:CMS

内存分配的并发问题

堆内存是线程共享的,所以在创建对象分配内存的时候一个重要的问题就是线程安全问题。JVM采用以下两种方式保证线程安全。

    1. CAS+失败重试:CAS是乐观锁的一种实现方式。乐观锁,就是每次假设没有冲突,不加锁地去执行操作。JVM采用CAS+失败重试的方式保证更新操作的原子性。
    2. 线程本地分配缓存(TLAB):JVM为每个线程预先在Eden区分配一小块区域-线程本地分配缓存(TLAB),JVM在给线程中的对象分配内存时,首先在  TLAB中划分内存。当对象大于TLAB剩余内存或者TLAB用尽时,JVM会再采用CAS+失败重试当方式分配内存。

1.3、初始化零值

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

1.4、设置头对象

初始化零值后,JVM要对对象信息进行必要的设置(与类的关联关系、关联类的元数据信息、对象的哈希码、对象的GC分代年龄),这些信息存在对象的对象头中。

1.4.1、头对象的形式

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

1.4.1.1、普通对象

|--------------------------------------------------------------|

|                     Object Header (64 bits)                  |

|------------------------------------|-------------------------|

|        Mark Word (32 bits)         |    Klass Word (32 bits) |

|------------------------------------|-------------------------|

1.4.1.2、数组对象

|------------------------------------------------------------------------------|

|                                                   Object Header (96 bits)                         |

|--------------------------------|-----------------------|----------------------|

|     Mark Word(32bits)    |  Klass Word(32bits)   |  array length(32bits) |

|--------------------------------|-----------------------|----------------------|

1.4.2、头对象组成

    1. Mark Word
    2. 指向类的指针
    3. 数组长度(只有数组对象才有)

1.4.2.1、Mark Word

Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。

Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。

JVM一般是这样使用锁和Mark Word的:

    1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
    2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
    3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
    4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
    6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
    7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

32位JVM中,Mark Word存储示意:

其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。

JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。

1.4.2.2、指向类的指针

该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。

Java对象的类数据保存在方法区。

1.4.2.3、数组长度

只有数组对象保存了这部分数据。

该数据在32位和64位JVM中长度都是32bit。

1.5、执行init方法

上述过程执行完,对象实例便已经创建出来了,但是所有的成员变量(属性字段)还都是零值。所以在new命令执行完之后,还需要执行方法对类的成员变量进行初始化,至此一个类的实例对象就创建完成了。


评论