HotSpot虚拟机对象详解

HotSpot虚拟机对象详解

HotSpot虚拟机对象创建需经类加载、内存分配(指针碰撞/空闲列表)、初始化及构造方法;内存布局包含对象头(Mark Word、类型指针)、实例数据与对齐填充;访问采用直接指针,优化性能。

一、对象创建过程

HotSpot虚拟机中,对象的创建遵循以下步骤:

  1. 类加载检查

    • 当遇到new指令时,首先检查该指令的参数(类符号引用)是否能在常量池中定位到对应的类。
    • 检查该类是否已被加载、解析和初始化。若未加载,则先执行类加载过程。
  2. 内存分配

    • 虚拟机为新生对象分配内存。分配方式取决于堆内存是否规整:

      • 指针碰撞(Bump the Pointer) :适用于堆内存规整(如使用Serial、ParNew等带压缩整理的收集器)。通过移动指针来分配内存。
      • 空闲列表(Free List) :适用于堆内存不规整(如使用CMS收集器)。维护一个列表记录可用内存块。
    • 并发处理:为避免多个线程分配内存冲突,采用两种策略:

      • CAS(Compare And Swap)重试:通过原子操作保证指针更新的正确性。
      • TLAB(Thread Local Allocation Buffer) :为每个线程预先分配一小块内存(-XX:+UseTLAB),线程私有,避免竞争。
  3. 内存空间初始化

    • 将分配的内存空间初始化为零值(不包括对象头)。确保实例字段在不赋初值时可直接使用(如int默认0)。
  4. 设置对象头(Object Header)

    • 对象头包含两类信息:

      • Mark Word:存储对象自身运行时数据:

        • 哈希码(Hash Code)
        • GC分代年龄(Generational Age)
        • 锁状态标志(如偏向锁、轻量级锁、重量级锁)
        • 线程持有的锁、偏向线程ID、偏向时间戳等
      • 类型指针(Class Pointer) :指向方法区的类元数据,确定对象属于哪个类。

    • 若启用压缩指针(-XX:+UseCompressedOops),类型指针占用4字节(否则8字节)。

  5. 执行构造方法()

    • 调用对象的构造函数(即<init>方法),按程序员的意愿初始化对象字段。
    • 此时对象才完全成为业务逻辑中的有效实例。

二、对象内存布局

HotSpot对象在堆内存中的存储布局分为三部分:

  1. 对象头(Header)

    • Mark Word(8/16字节):

      • 32位虚拟机:4字节;64位虚拟机:8字节(开启压缩指针则为4字节)。

      • 内容随锁状态变化,例如:

        锁状态 存储内容
        无锁 哈希码、分代年龄、偏向模式0
        偏向锁 偏向线程ID、时间戳、分代年龄、偏向模式1
        轻量级锁 指向栈中锁记录的指针
        重量级锁 指向互斥量(Monitor)的指针
        GC标记 空(用于GC标记阶段)
    • 类型指针(4/8字节):指向方法区的类元数据,用于确定对象类型。

    • 数组长度(可选):若对象是数组,额外4字节记录数组长度。

  2. 实例数据(Instance Data)

    • 对象真正存储的有效信息,即类中定义的各种字段内容(包括继承的父类字段)。

    • 字段存储顺序受虚拟机分配策略(FieldsAllocationStyle)影响:

      • 相同宽度的字段分配在一起(如longdouble优先,intfloat次之)。
      • 父类字段出现在子类之前(-XX:CompactFields=true时,允许子类较窄字段插入父类空隙)。
  3. 对齐填充(Padding)

    • 起占位作用,确保对象总大小为8字节的整数倍。
    • 原因:HotSpot要求对象起始地址对齐(如8字节对齐),提升内存访问效率。

示例:一个简单对象的内存布局

arduino 复制代码
class Person {
    int age;          // 4字节
    String name;      // 引用类型(压缩后4字节)
}

假设在64位JVM启用压缩指针,对象布局如下:

  • 对象头:Mark Word(8字节) + 类型指针(4字节) = 12字节
  • 实例数据:age(4字节) + name(4字节) = 8字节
  • 对齐填充:填充4字节,总大小为24字节(12+8+4=24,8的倍数)。

三、对象访问定位

Java程序通过栈上的引用(Reference)访问堆中对象,具体方式有两种:

  1. 句柄访问

    • 实现:堆中划分句柄池,每个句柄包含两个指针:

      • 指向对象实例数据(Instance Data)的指针。
      • 指向方法区类型数据(Class Metadata)的指针。
    • 优点:对象移动(如GC)时,只需更新句柄中的指针,无需修改栈中的引用。

    • 缺点:访问需两次指针跳转,性能略低。

  2. 直接指针访问(HotSpot采用方式)

    • 实现:引用直接存储对象地址,对象头中的类型指针指向方法区类元数据。
    • 优点:访问速度更快(减少一次指针定位)。
    • 缺点:对象移动时需更新所有引用(通过GC的移动算法解决,如复制、标记-整理)。

对比示意图

复制代码
句柄访问:
栈引用 → 句柄池 → 实例数据 & 类型数据
​
直接指针访问:
栈引用 → 实例数据(含类型指针) → 类型数据

四、总结

  • 对象创建:需经历类加载检查、内存分配、初始化、设置对象头和执行构造方法。
  • 内存布局:分为对象头(运行时数据和类型指针)、实例数据和对齐填充。
  • 访问定位:HotSpot选择直接指针访问以优化性能,依赖对象头中的类型指针快速定位类元数据。

关键参数与工具

  • -XX:+UseCompressedOops:启用压缩指针(默认开启)。

  • -XX:FieldsAllocationStyle:调整字段分配顺序。

  • JOL(Java Object Layout)工具:分析对象内存布局(示例输出):

    arduino 复制代码
    java -jar jol-cli.jar internals java.lang.String
相关推荐
Java中文社群11 分钟前
SpringAI用嵌入模型操作向量数据库!
后端·aigc·openai
暴力袋鼠哥20 分钟前
基于Flask的跨境电商头程预警分析系统
后端·python·flask
一只爱撸猫的程序猿1 小时前
防止外部API服务不可用拖垮系统的解决方案
spring boot·后端·程序员
白露与泡影1 小时前
SpringBoot 最大连接数及最大并发数是多少?
spring boot·后端·firefox
radient1 小时前
线上死锁问题排查思路
后端
逆风局?1 小时前
Spring-AOP-面相切面编程
java·后端·spring
Golang菜鸟2 小时前
golang中的组合多态
后端·go
lamdaxu2 小时前
Java集合--TreeSet&TreeMap源码解析
后端
独立开阀者_FwtCoder2 小时前
深入解密Node共享内存:这个原生模块让你的多进程应用性能翻倍
前端·javascript·后端
Asthenia04122 小时前
深入剖析 Spring Cloud Feign 的 Contract 组件:设计与哲学
后端