JVM 之 HotSpot虚拟机对象在Java堆中分配、布局和访问的全过程【对象分配的具体流程?内存分配时有哪些策略?如何解决并发问题?对象布局?】

28 阅读5分钟

HotSpot虚拟机对象在Java堆中分配、布局和访问的全过程。

总结:

1、对象的创建:

  • 类加载检查(没加载则加载,获取对象所需内存)
  • -》内存分配(指针碰撞、空闲列表,并发通过TLAB+CAS重试 解决)
  • -》对象初始化(设置对象头信息+其他信息零值填充)
  • -》调用构造器(dup指令复制对象指针+invokespecial指令调用对象构造方法)。

2、对象的内存分布:

  • 对象头(mark word(哈希码+GC年龄+锁标志位)+
  • 类元数据指针+数组长度)+实例数据(根据字段类型排序)+
  • 对齐填充(保证是8字节的倍数)

3、对象的访问定位:

  • 句柄访问:reference访问堆中句柄池中的对象句柄,然后在找到堆中对象
  • 直接指针访问:reference直接存储对象地址。

详解:

如何进行对象的创建?

创建对象需要解决的问题?

  • 1、先知道创建对象需要多少内存? -> 类加载检查 -> 获取类信息,如不存在就类加载后再获取类信息 -> 类信息就决定了对象所需要的内存大小
  • 2、怎么给对象进行内存分配?-> 分配策略:指针碰撞(简单高效 0(1))、空闲列表(0(n))
  • 3、分配时的并发问题怎么解决?-> TLAB(线程本地缓冲)+ CAS重试
  • 4、对象如何初始化?-> 设置对象头信息(类元数据指针、GC年龄、哈希值、锁标志位)+ 零值填充其他信息
  • 5、对象怎么提供给程序进行访问? -> dup(复制引用)-> invokespecial(通过dup复制的引用调用构造器方法)

对象创建流程?其中如何解决的上述问题?

  • 1、先进行类加载检查‌
    • 解析‌指令(获取类引用):遇到new指令时,虚拟机从常量池中获取类的引用。
    • 验证引用(是否需要类加载):验证该类是否已加载、解析、初始化(通过方法)。未加载则触发类加载过程。
  • 2、然后进行内存分配‌:(类加载后便可以确定对象的大小(例如,int占用4字节),所以可以直接进行空间分配)
    • 2.1、分配策略‌:

      • 指针碰撞‌(规整空间):移动指针分配内存,简单高效,O(1)(如:Serial、ParNew等带压缩整理过程的收集器)。
      • 空闲列表‌(碎片化空间):从空闲列表中分配,需遍历空闲列表,所以O(n)(如:CMS GC,基于清除(Sweep)算法的收集器)。
        • 注意: 其实,CMS设计了一个叫作Linear Allocation Buffer的分配缓冲区,在通过空闲列表拿到一大块分配缓冲区后,在这块缓冲区里可以使用指针碰撞方式来分配。 在这里插入图片描述
    • 2.2、如何保证分配时的并发安全‌?

      • 当线程创建对象时,优先从TLAB(线程本地分配缓冲)分配(每个线程预分配内存块,减少全局锁竞争,内存连续性
      • 若TLAB不足,则通过**CAS重试(原子操作 无锁高效)**向Eden区申请新TLAB。
      • 注意:是否使用TLAB,可以通过**-XX:+/-UseTLAB**参数来设定
  • 3、再然后初始化‌对象
    • 设置‌对象头:记录以下信息
      • 类元数据指针:指向方法区的类元数据(32位JVM占用4字节,64位JVM占用8字节)
      • GC年龄:Minor GC的次数(默认15次后升到老年代,可通过-XX:MaxTenuringThreshold调整
      • 哈希值:延迟计算,首次调用System.identityHashCode()时生成
      • 锁标志位:偏向锁ID、轻量级锁、重量级锁等信息
    • 零值填充‌(除对象头外设置初始值):分配内存后,除对象头外初始化为零值(如int默认为0)。
  • 4、最后调用构造器‌:字节码指令(new → dup → invokespecial)
    • dup指令‌:复制引用,确保初始化后可被程序使用。
    • 构造方法‌:通过invokespecial指令,使用dup复制的对象引用 去调用构造方法。

new vs 反射 vs 克隆创建对象时的异同

在这里插入图片描述

如何进行对象的内存布局?

对象内存布局时需要考虑的问题?

  • 1、对象的基本信息有哪些?-> 运行时数据(哈希码、GC年龄、锁状态)、类元数据指针(如何找到对象的类信息)、数组的长度(对象可确定大小,但数组对象需根据对象和数量确定大小)
  • 2、基本信息存在哪?-> 对象头。
  • 3、对象数据如何存储?-> 按字段类型的大小排序后存储(longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs))
  • 4、如何适配64位机器?-> 通过对齐填充保证为8字节的倍数。

Java对象内存详解

  • 1、对象头:
    • Mark Word‌:存储运行时数据(31bit哈希码、4bit代龄、2bit锁标志、26bit未使用)。
    • 类元数据指针‌:指向方法区的类元数据。
    • 数组长度‌仅数组对象有
  • 2、实例数据:
    • 存储类的字段(如int a; String b;)。
    • 按字段类型和顺序排列(longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs))。
  • 3、对齐填充:确保对象起始地址为8字节倍数(如long对齐)。

如何进行对象的访问定位?

java通过栈上的reference数据来操作堆上的具体对象.

访问对象的方式有哪些?各自怎么实现的?各自的优缺点?

  • 1、句柄访问:通过reference找到java堆中句柄池内该对象的句柄(句柄 = 对象实例数据地址信息 + 类元数据地址信息)

  • 优点:对象变动时,只需修改句柄中的地址信息,reference不需要变动

  • 缺点:需先找到句柄,然后再找到对象,多了一次指针定位

  • 在这里插入图片描述

  • 2、直接指针访问:reference中直接存放对象地址

  • 优点:相比句柄节省了一次指针定位的时间开销,更快

  • 缺点:对象变动时,需修改reference。 在这里插入图片描述