HotSpot虚拟机对象详解
HotSpot虚拟机对象创建需经类加载、内存分配(指针碰撞/空闲列表)、初始化及构造方法;内存布局包含对象头(Mark Word、类型指针)、实例数据与对齐填充;访问采用直接指针,优化性能。
一、对象创建过程
HotSpot虚拟机中,对象的创建遵循以下步骤:
-
类加载检查
- 当遇到
new
指令时,首先检查该指令的参数(类符号引用)是否能在常量池中定位到对应的类。 - 检查该类是否已被加载、解析和初始化。若未加载,则先执行类加载过程。
- 当遇到
-
内存分配
-
虚拟机为新生对象分配内存。分配方式取决于堆内存是否规整:
- 指针碰撞(Bump the Pointer) :适用于堆内存规整(如使用Serial、ParNew等带压缩整理的收集器)。通过移动指针来分配内存。
- 空闲列表(Free List) :适用于堆内存不规整(如使用CMS收集器)。维护一个列表记录可用内存块。
-
并发处理:为避免多个线程分配内存冲突,采用两种策略:
- CAS(Compare And Swap)重试:通过原子操作保证指针更新的正确性。
- TLAB(Thread Local Allocation Buffer) :为每个线程预先分配一小块内存(-XX:+UseTLAB),线程私有,避免竞争。
-
-
内存空间初始化
- 将分配的内存空间初始化为零值(不包括对象头)。确保实例字段在不赋初值时可直接使用(如
int
默认0)。
- 将分配的内存空间初始化为零值(不包括对象头)。确保实例字段在不赋初值时可直接使用(如
-
设置对象头(Object Header)
-
对象头包含两类信息:
-
Mark Word:存储对象自身运行时数据:
- 哈希码(Hash Code)
- GC分代年龄(Generational Age)
- 锁状态标志(如偏向锁、轻量级锁、重量级锁)
- 线程持有的锁、偏向线程ID、偏向时间戳等
-
类型指针(Class Pointer) :指向方法区的类元数据,确定对象属于哪个类。
-
-
若启用压缩指针(-XX:+UseCompressedOops),类型指针占用4字节(否则8字节)。
-
-
执行构造方法()
- 调用对象的构造函数(即
<init>
方法),按程序员的意愿初始化对象字段。 - 此时对象才完全成为业务逻辑中的有效实例。
- 调用对象的构造函数(即
二、对象内存布局
HotSpot对象在堆内存中的存储布局分为三部分:
-
对象头(Header)
-
Mark Word(8/16字节):
-
32位虚拟机:4字节;64位虚拟机:8字节(开启压缩指针则为4字节)。
-
内容随锁状态变化,例如:
锁状态 存储内容 无锁 哈希码、分代年龄、偏向模式0 偏向锁 偏向线程ID、时间戳、分代年龄、偏向模式1 轻量级锁 指向栈中锁记录的指针 重量级锁 指向互斥量(Monitor)的指针 GC标记 空(用于GC标记阶段)
-
-
类型指针(4/8字节):指向方法区的类元数据,用于确定对象类型。
-
数组长度(可选):若对象是数组,额外4字节记录数组长度。
-
-
实例数据(Instance Data)
-
对象真正存储的有效信息,即类中定义的各种字段内容(包括继承的父类字段)。
-
字段存储顺序受虚拟机分配策略(FieldsAllocationStyle)影响:
- 相同宽度的字段分配在一起(如
long
和double
优先,int
和float
次之)。 - 父类字段出现在子类之前(-XX:CompactFields=true时,允许子类较窄字段插入父类空隙)。
- 相同宽度的字段分配在一起(如
-
-
对齐填充(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)访问堆中对象,具体方式有两种:
-
句柄访问
-
实现:堆中划分句柄池,每个句柄包含两个指针:
- 指向对象实例数据(Instance Data)的指针。
- 指向方法区类型数据(Class Metadata)的指针。
-
优点:对象移动(如GC)时,只需更新句柄中的指针,无需修改栈中的引用。
-
缺点:访问需两次指针跳转,性能略低。
-
-
直接指针访问(HotSpot采用方式)
- 实现:引用直接存储对象地址,对象头中的类型指针指向方法区类元数据。
- 优点:访问速度更快(减少一次指针定位)。
- 缺点:对象移动时需更新所有引用(通过GC的移动算法解决,如复制、标记-整理)。
对比示意图:
句柄访问:
栈引用 → 句柄池 → 实例数据 & 类型数据
直接指针访问:
栈引用 → 实例数据(含类型指针) → 类型数据
四、总结
- 对象创建:需经历类加载检查、内存分配、初始化、设置对象头和执行构造方法。
- 内存布局:分为对象头(运行时数据和类型指针)、实例数据和对齐填充。
- 访问定位:HotSpot选择直接指针访问以优化性能,依赖对象头中的类型指针快速定位类元数据。
关键参数与工具:
-
-XX:+UseCompressedOops
:启用压缩指针(默认开启)。 -
-XX:FieldsAllocationStyle
:调整字段分配顺序。 -
JOL(Java Object Layout)工具:分析对象内存布局(示例输出):
arduinojava -jar jol-cli.jar internals java.lang.String