首页 > 基础资料 博客日记

【Java】关于Java对象的创建过程

2024-08-21 10:00:06基础资料围观165

文章【Java】关于Java对象的创建过程分享给大家,欢迎收藏Java资料网,专注分享技术知识

1、对象的创建

在 Java 中,创建对象的方式有很多种,比如最常见的通过new xxx()来创建一个对象,通过反射Class.forName(xxx).newInstance()来创建对象等。其实无论是哪种创建方式,JVM 底层的执行过程是一样的。
对象的创建过程,可以用如下图来简要概括:

创建对象大致分为 5 个步骤:

  1. 检查类是否加载(非必然步骤,如果没有就执行类的加载);
  2. 分配内存;
  3. 初始化零值;
  4. 设置头对象;
  5. 执行<init>方法(该方法由实例成员变量声明、实例初始化块和构造方法组成)。

下面我们一起来看下每个步骤具体的工作内容。

1.1 类加载检查

当需要创建一个类的实例对象时,比如通过new xxx()方式,虚拟机首先会去检查这个类是否在常量池中能定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化,如果没有,那么必须先执行类的加载流程;如果已经加载过了,就不会再次加载。

Q:为什么在对象创建时,需要有这一个检查判断?
A:主要原因在于:类的加载,通常都是懒加载,只有当使用类的时候才会加载,所以先要有这个判断流程。

1.2 分配内存

类加载成功后,虚拟机就能够确定对象的大小了,此时虚拟机会在堆内存中划分一块对象大小的内存空间出来,分配给新生对象。
虚拟机如何在堆中分配内存主要有两种方式:

  1. 指针碰撞法
  2. 空闲列表法
    下面我们一起来看看相关的内存分配方式。
1.2.1 指针碰撞法

如果内存是规整的,那么虚拟机将采用指针碰撞法来为对象分配内存。指针碰撞法,简单的说就是所有用过的内存在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存时会把指针向空闲一方挪动一段,直到能容纳对象大小的位置。
如果垃圾收集器选择的是 SerialParNew 这种基于压缩算法的,虚拟机会采用这种分配方式。

1.2.2 空闲列表法

如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用空闲列表法来为对象分配内存。空闲列表法,简单的说就是在虚拟机内部维护了一个列表,会记录哪些内存块是可用的,在分配的时候会从列表中找到一块能容纳对象大小的空间,划分给对象实例,并更新列表上的内容。
如果垃圾收集器选择的是 CMS 这种基于标记-清除算法的,虚拟机会采用这种分配方式。

1.2.3 内存分配安全问题

我们知道,虚拟机是支持多个线程同时分配内存的,是否会有线程安全的问题呢?
答案是:肯定存在的。比如用指针碰撞法时,虚拟机正在给对象 A 分配内存,但指针还没来及修改,此时又有一个线程给对象 B 分配内存,同时使用了原来的指针来分配,最后的结果就是这个区域只分配来一个对象,另一个对象被覆盖了。
针对内存分配时存在的线程安全问题,虚拟机采用了两种方式来进行处理:

  • CAS+重试机制:通过 CAS 操作移动指针,只有一个线程可以移动成功,移动失败的线程重试,直到成功为止
  • TLAB (thread local Allocation buffer):也称为本地线程分配缓冲,这个处理方式思想很简单,就是当线程开启时,虚拟机会为每个线程分配一块较大的空间,然后线程内部创建对象的时候,就从自己的空间分配,这样就不会有并发问题了,当线程自己的空间用完了才会从堆中分配内存,之后会转为通过 CAS+重试机制来解决并发问题

以上就是虚拟机解决对象内存分配时存在的线程安全问题的措施。

1.2.4 内存结构

当JVM分配好内存后,这个对象在内存中已经存在了。以 32 位的虚拟机为例(64位的虚拟机略有不同),对象的内存结构可以用如下图来简要概括:

各部分区域功能如下:

  • 对象头:分为 Mark Word 和元数据区,如果是数组对象,还有记录数组长度的区域。这三块保存着对象的 hashCode 值,锁的状态,类元数据指针,对象的分代年龄等等信息;
  • 实例数据:顾名思义,用于保存对象成员变量的值,如果变量是引用类型,保存的是内存地址;
  • 对齐填充位:因为 HotSpot 虚拟机要求对象的起止地址必须是 8 字节的整数倍,也就是要求对象的大小为 8 字节的整数倍,如果不足 8 字节的整数倍,那么就需要通过对齐填充进行占位,补够 8 字节的整数倍。

注意:其实到内存分配这一步完成,这个对象算已经创建好了,但只是个雏形,还不能使用。

1.3 初始化零值

初始化零值,顾名思义,就是对分配的这一块内存(实例数据区)初始化零值,也就是给对象的实例成员变量赋于零值,比如 int 类型赋值为 0,引用类型为null等操作。这样对象就可以在没有赋值情况下使用了,只不过访问对象的成员变量都是零值。

1.4 设置头对象

初始化零值完成之后,虚拟机就会对对象进行必要的设置,比如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息,这些信息都会存放在对象头中。这部分数据,官方称它为“Mark Word”。
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头 (Header)实例数据 (Instance Data)对齐填充位 (Padding)

我们来看下 Mark Word 的组成,不同的操作系统环境下占用的空间不同,在 32 位操作系统中占 4 个字节,在 64 位中占 8 个字节。

以 32 位操作系统为例,Mark Word 内部结构如下:

各部分的含义如下:

  • identity_hashcode:25 位的对象标识哈希码。采用延迟加载技术,调用System.identityHashCode()方法获取,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程 Monitor 中;
  • age:4 位的 Java 对象年龄。在GC中,如果对象在 Survivor 区复制一次,年龄增加 1,当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行 GC 的年龄阈值为 15,并发 GC 的年龄阈值为 6。由于 age 只有4位,所以最大值为15,这就是为什么-XX:MaxTenuringThreshold选项最大值为 15 的原因;
  • lock:2 位的锁状态标记位。对象的加锁状态分为无锁、偏向锁、轻量级锁、重量级锁等几种标记,不同的标记值,表示的含义也不同;
  • biased_lock:对象是否启用偏向锁标记,只占 1 个二进制位。为 1 时表示对象启用偏向锁,为 0 时表示对象没有偏向锁。偏向锁是一种锁的优化手段,开启偏向锁,某些时候可以省去线程频繁申请锁的操作,提升程序执行性能。;
  • thread:持有偏向锁的线程 ID,如果该线程再次访问这个锁的代码块,可以直接访问;
  • epoch:偏向锁在 CAS 锁操作过程中的标识;
  • ptr_to_lock_record:在轻量级锁时,指向栈中锁记录的指针;
  • ptr_to_heavyweight_monitor:在重量级锁时,指向管程 Monitor 的指针。

其中biased_locklock参数中不同的标记值,表示的含义如下:

lock标记位,通常会在使用到synchronized关键字的对象上发挥作用。随着线程之间竞争激烈程度,对象锁会从无锁状态逐渐升级到重量级锁,其中的变化过程,可以用如下步骤来概括:

  1. 初期锁对象刚创建时,还没有任何线程来竞争,锁状态为 01,偏向锁标识位是0(无线程竞争它),此时程序执行效率最高。
  2. 当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率也非常高。
  3. 当有两个线程开始竞争这个锁对象时,情况会发生变化,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的 Mark Word 就执行哪个线程的栈帧中的锁记录。轻量级锁在加锁过程中,用到了自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁,执行效率有所衰减。
  4. 如果竞争这个锁对象的线程越来越多,会导致更多的切换和等待,JVM 会把该对象的锁升级为重量级锁。这个就是大家常说的同步锁,此时对象中的 Mark Word 会再次发生变化,会指向一个监视器 (Monitor) 对象,这个监视器对象用集合的形式来登记和管理排队的线程。Monitor 依赖操作系统的 MutexLock(互斥锁)来实现线程排队,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换线程,相比其它级别的锁,此时锁的性能最差。

1.5 执行<init>方法

<init>方法Java在编译的时候生成的,该方法包含这个类中的实例成员变量声明、实例初始化块和构造方法,作用是给对象执行初始化操作。类中有多少个构造方法就有多少个<init>方法。创建对象时使用哪个构造方法,就执行对应的<init>方法。<init>方法中的语句顺序与实例成员变量初始化顺序一致,下图是实例成员变量的初始化顺序:

当然父类也有<init>方法,初始化对象时,先执行父类的<init>方法再执行子类的<init>方法,如图所示:

到这里初始化操作完成之后,Java对象才算真正意义上创建了,这时候才能够使用这个对象。

顺便扩展一下前面的类加载阶段时的静态成员变量初始化。静态成员变量初始化对应的是<clinit>方法,并且也是JVM自动生成的。<clinit>方法中的语句顺序与静态成员变量初始化顺序一致,下图是静态成员变量的初始化顺序:

注意<clinit>方法不会在创建对象时执行,只有在类加载的初始化阶段时候,才会执行对应的<clinit>方法。具体查看Java类的初始化时机

最后,如果我们想看下对象创建后的大小,可以添加第三方jol包,使用它来打印对象的内存布局情况。

<dependency>
   <groupId>org.openjdk.jol</groupId>
   <artifactId>jol-core</artifactId>
   <version>0.9</version>
</dependency>

编写一个测试类:

public class ObjectHeaderTest {

    public static void main(String[] args) {
        System.out.println("=========打印Object对象的大小========");
        ClassLayout layout = ClassLayout.parseInstance(new Object());
        System.out.println(layout.toPrintable());


        System.out.println("========打印数组对象的大小=========");
        ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
        System.out.println(layout1.toPrintable());


        System.out.println("========打印有成员变量的对象大小=========");
        ClassLayout layout2 = ClassLayout.parseInstance(new ArtisanTest());
        System.out.println(layout2.toPrintable());
    }

    /**
     * ‐XX:+UseCompressedOops 表示开启压缩普通对象指针
     * ‐XX:+UseCompressedClassPointers 表示开启压缩类指针
     *
     */
    public static class ArtisanTest {

        int id;        //4B
        String name;   //4B
        byte b;        //1B
        Object o;      //4B
    }
}

运行结果:

=========打印Object对象的大小========
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

========打印数组对象的大小=========
[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     0    int [I.<elements>                             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

========打印有成员变量的对象大小=========
com.example.myspringboot001.test.ObjectHeaderTest$ArtisanTest object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           61 e0 00 f8 (01100001 11100000 00000000 11111000) (-134160287)
     12     4                int ArtisanTest.id                            0
     16     1               byte ArtisanTest.b                             0
     17     3                    (alignment/padding gap)                  
     20     4   java.lang.String ArtisanTest.name                          null
     24     4   java.lang.Object ArtisanTest.o                             null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

参考:JVM系列(四) -对象创建过程详解


文章来源:https://blog.csdn.net/qq_53070263/article/details/137514717
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云