JVM中的Java对象在堆内存中的存储分布可以分为对象头,实例数据和对齐填充三部分
对象头:
包含运行时元数据和类型指针
1、Mark Word(标记字段)
对象自身的运行时数据:
- 锁状态标志(无锁、偏向锁、轻量级锁、重量级锁、GC标记)
- 哈希码
- 分代年龄(用于分代GC)
- 线程ID(偏向锁持有者)
长度:
- 32位JVM:4字节
- 64位JVM:8字节,开启指针压缩(-XX:+UseCompressedOops)后可能优化为4字节
数组长度(仅数组对象有)
实例数据
存储对象的字段数据(包括从父类继承的字段)排列顺序受,字段分配策略 影响:
- 默认策略:按
long/double
→int/float
→short/char
→byte/boolean
→引用类型
降序排列。 - 父类字段在前,子类字段在后。
- 相同宽度的字段会被分配在一起(减少内存填充)。
对齐填充:
- JVM要求对象大小必须是8字节的整数倍,如果对象头+实例数据总大小不满足会额外填充字节。
- 作用:优化内存访问性能(CPU按块读取,对齐后减少缓存行未命中)
对象头与锁的关系:
每个锁对象的对象头都会记录当前锁的类型
若该锁为偏向锁
直接在锁对象的Mark Word中记录对应线程id,减少竞争
若该锁为轻量级锁
参与竞争的线程栈(jvm栈)的栈帧中会创建一个Lock Record
struct LockRecord {markOop displacedHeader; // 存储原Mark Word(Displaced Mark Word)oop owner; // 持有锁的线程标识(或对象引用)int nest; // 重入计数EntryQ entryQueue; // 临时阻塞队列,关联系统互斥锁(竞争失败时阻塞线程)void* obj; // 关联Java对象int rcThis; // 阻塞/等待的线程数int candidate; // 优化唤醒策略(0/1)[1,3](@ref)...其他属性
};
获取锁:
- 线程会将锁对象的mark word复制一份到displaceHeader属性中
- 线程尝试将锁对象的mark word地址指向自己的LockRecord中
- 如果失败则说明锁已被其他线程获取,当前线程自旋或锁膨胀为重量级锁(例如CAS自旋超过十次)
释放锁:
- 当nest将自减为0时,也就是将要释放时,将mark word指向锁对象原来的地址,并清空displacedHeader
- 当displaceHeader为null时,说明当前线程已将锁释放
若该锁为重量级锁:
当jvm感到锁竞争激烈时(例如有线程CAS自旋次数超过十次),会为锁对象分配一个ObjectMonitor,并让mark word指向ObjectMonitor
struct ObjectMonitor {markOop _header; // 存储原Mark Wordvoid* _owner; // 持有锁的线程标识(或对象引用)intptr_t _count; // 重入计数intptr_t _waiters; // 等待线程数void* _EntryList; // 关联系统级互斥锁(竞争失败时阻塞线程)void* _WaitSet; // 等待队列void* _cxq; // 竞争队列(新线程竞争锁的临时队列,FILO栈) void* _object; // 关联的Java对象...其他属性
};
竞争失败的队列会进入_EntryList中阻塞,等待锁释放后通过公平/非公平的方式唤醒。
- 非公平方式:从_EntryList或_cxq队列中随机唤醒一个线程,被唤醒的线程直接尝试获取锁,而非通过CAS竞争
- 公平方式:线程按入队顺序获取锁