一、对象的内存布局
Java对象在内存中的存储布局可分为三个部分:
1. 对象头(Header)
对象头包含两类信息:
第一类:存储对象自身的运行时数据
-
哈希码(HashCode)
-
GC分代年龄
-
锁状态标志
-
线程持有的锁
-
偏向线程ID
-
偏向时间戳等
这部分数据在32位和64位虚拟机中分别占用32bit和64bit,官方称为"Mark Word"。
第二类:类型指针
-
指向对象类型元数据的指针
-
虚拟机通过这个指针确定对象是哪个类的实例
-
如果是Java数组,对象头还必须额外包含一块用于记录数组长度的数据
2. 实例数据(Instance Data)
-
对象真正存储的有效信息
-
即代码中定义的各种类型的字段内容
-
包括从父类继承下来的字段
3. 对齐填充(Padding)
-
不是必然存在的部分
-
仅起占位符作用
-
HotSpot VM要求对象起始地址必须是8字节的整数倍
-
对象头已被设计为8字节的倍数(1倍或2倍)
-
当实例数据部分未对齐时,需要填充来补全
二、对象内存分配方式
1. 指针碰撞(Bump the Pointer)
适用条件:内存规整(所有使用过的内存在一边,空闲内存在另一边)
工作原理:
-
使用一个指针作为分界点指示器
-
分配内存仅需将指针向空闲方向移动对象大小的距离
优点:简单高效
缺点:
-
需要内存规整(连续)
-
多线程环境下需要同步处理
解决方案:
-
采用CAS(Compare And Swap)重试保证原子性
-
或使用TLAB(Thread Local Allocation Buffer)策略
2. 空闲列表(Free List)
适用条件:内存不规整(已使用和空闲内存交错)
工作原理:
-
虚拟机维护一个列表记录可用内存块
-
分配时从列表中找到足够大的空间划分给对象
-
更新列表记录
优点:适应不规则内存
缺点:
-
会产生内存碎片
-
大对象可能难以找到合适空间
3. TLAB策略(Thread Local Allocation Buffer)
-
为每个线程预先分配一小块内存
-
线程分配对象优先在自己的TLAB中分配
-
TLAB用完时,才需要同步锁定分配新的TLAB
优点:
-
减少线程竞争
-
提高分配效率
分配方式的选择依据
分配方式 | 适用条件 | 典型收集器 |
---|---|---|
指针碰撞 | 内存规整 | Serial、ParNew等带压缩功能的收集器 |
空闲列表 | 内存不规整 | CMS等基于清除算法的收集器 |
选择取决于垃圾收集器是否具有空间压缩整理能力。
三、对象的访问定位
1. 使用句柄访问
实现方式:
-
Java堆中划分一块内存作为句柄池
-
reference存储对象的句柄地址
-
句柄包含对象实例数据和类型数据的具体地址
访问流程:
-
通过栈中reference找到句柄
-
通过句柄访问实例数据
-
通过句柄访问类型数据
优点:
-
reference稳定(对象移动时只需更新句柄)
-
内存整理时不需要修改reference
缺点:
-
访问需要三次定位
-
性能开销较大
2. 直接指针访问
实现方式:
-
reference直接存储对象地址
-
对象内存布局中需要包含类型数据信息
访问流程:
-
通过栈中reference直接访问对象实例
-
通过对象头中的类型指针访问类型数据
优点:
-
访问速度快(减少一次间接访问)
-
HotSpot虚拟机主要采用这种方式
缺点:
-
对象移动时需要修改reference
四、总结对比
特性 | 句柄访问 | 直接指针访问 |
---|---|---|
访问速度 | 较慢(3次) | 较快(2次) |
对象移动影响 | reference不变 | 需修改reference |
内存占用 | 额外句柄池空间 | 更节省空间 |
实现复杂度 | 较高 | 较低 |
HotSpot虚拟机主要采用直接指针访问方式,因为访问速度是对象访问的主要性能考量因素。理解这些底层机制有助于我们更好地优化Java应用性能,特别是在处理大量对象创建和访问的场景时。