持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情
一、对象的内存布局
Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象才有)等。
- 实例数据:存放类的属性数据信息,包括父类的属性信息;
- 对齐填充:由于虚拟机要求
对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
1-1、对象头详解
HotSpot虚拟机的对象头包括:
- Mark Word
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为“Mark Word”。
- Klass Pointer
对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。
- 数组长度(只有数组对象有)
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。 4字节
1-2、使用JOL工具查看内存布局
可以查看普通java对象的内部布局工具JOL(JAVA OBJECT LAYOUT),使用此工具可以查看new出来的一个java对象的内部布局,以及一个普通的java对象占用多少字节。
1-2-1、引入maven依赖
<!-- 查看Java 对象布局、大小工具 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
1-2-2、测试使用(默认开启指针压缩)
1-2-2-1、查看空对象内存存储
- 利用jol查看64位系统java对象(空对象),默认开启指针压缩,总大小显示16字节,前12字节为对象头,因为对象存储占用字节需要是8的整数倍,目前只有12字节,因此需要有4个字节的填充位,这样总大小就是16字节
以上输出各列的含义如下:
- OFFSET:偏移地址,单位字节;
- SIZE:占用的内存大小,单位为字节;
- TYPE DESCRIPTION:类型描述,其中object header为对象头;
- VALUE:对应内存中当前存储的值,二进制32位;
1-2-2-2、查看非空内存对象存储
可以看到对象头前8个字节为mark word,第三个4字节为klass point,最后一8字节为Test对象中的为long类型的p属性,因为long占用8字节,这样加上对象头的12字节,就是20字节,因为需要为8的整数倍,就需要补充位为4字节。
1-2-2-3、查看非空内存对象存储(多属性)
可以看到除对象头的mark word和klass point之外的12字节,就是Test对象中的String类型b(String 占用4字节),和long类型的p(long 占用8字节),这样计算起来正好是24字节,填充位就没有了。
1-2-3、关闭指针压缩后
对象头为16字节:-XX:-UseCompressedOops
二、Mark Word是如何记录锁状态的
通过以上的了解,锁状态被记录在每个对象的对象头的Mark Word中
Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。
简单点理解就是:MarkWord 结构搞得这么复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处。
2-1、Mark Word的结构
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
。。。。。。
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
- hash: 保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。
- age: 保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
biased_lock: 偏向锁标识位。由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。lock: 锁状态标识位。区分锁状态,比如11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。- JavaThread*: 保存持有偏向锁的线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
- epoch: 保存偏向时间戳。偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
2-1-1、32位JVM下的对象结构描述
2-1-2、64位JVM下的对象结构描述
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针。ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针
2-2、Mark Word中锁标记枚举
enum { locked_value = 0, //00 轻量级锁
unlocked_value = 1, //001 无锁
monitor_value = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁
marked_value = 3, //11 GC标记
biased_lock_pattern = 5 //101 偏向锁
更直观的理解方式:
32位和64位系统对象结构锁,都是以后两位来锁定对象锁的状态
2-3、测试:利用JOL工具跟踪锁标记变化
2-3-1、偏向锁
当JVM启用了偏向锁模式(jdk6默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。
偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。
2-3-1-1、未添加锁状态查看数据对象头锁标记
再次看下刚刚代码运行的结果,锁状态标记为001即为无锁状态。
2-3-2-1、添加锁-查看对象头锁标记
分别输出obj在加锁前和加锁后的对象信息,首先给obj加锁
下图中对象信息,一开始是001->无锁状态;加锁之后是000->轻量级锁。此时obj本应该是偏向锁的,单为何输出的是轻量级锁呢?这就设计到了 偏向锁延迟偏向的机制
2-3-2-2、偏向锁延迟偏向
偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式。JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。
2-3-2-2-1、关闭延迟偏向锁
- 方式一:设置JVM参数
关闭延迟偏向锁
//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
启用/禁止偏向锁
//禁止偏向锁
-XX:-UseBiasedLocking
//启用偏向锁
-XX:+UseBiasedLocking
在Idea中设置JVM参数
启动测试,这样在未加锁的时候obj对象就启动了偏向锁(锁指向为空),添加线程之后(锁指向即为对应的ID)
- 方式二:让线程等待5秒,让JVM先飞一会
因为Hotpoit虚拟机在启动后有个4秒延迟才会对每个新建的对象开启偏向锁,这样我们先让线程休眠一会就可以跳过4秒延迟,如下:
运行结果,可以看到和添加JVM,关闭延迟偏向锁-XX:BiasedLockingStartupDelay=0效果一致。
2-3-2-3、给类对象添加锁
给类对象添加锁
类对象的锁状态就是轻量级锁,如下:
2-4、偏向锁撤销
调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销。因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashcode的,如下图
2-4-1、添加hashCode()观察偏向锁
2-4-1-1、对象处于可偏向锁调用hashCode
以上代码执行结果如下:
通过上图,obj的101(可偏向锁状态)-->通过调用hashCode-->000(轻量级锁)
2-4-1-2、对象处于偏向锁的时候
以上代码执行结果如下:
通过上图,obj的101(可偏向锁状态)-->通过synchronized加锁变革为偏向锁-->通过调用hashCode--->010(重量级锁)
2-4-1-1、小结
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也无法偏向:
- 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
- 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。
2-4-2、使用wait/notify偏向锁的状态
2-4-2-1、使用wait
在偏向锁中使用wait休眠200毫秒
运行结果如下:
通过上图,obj的101(可偏向锁状态)-->通过synchronized加锁变革为偏向锁-->通过调用obj.wait--->010(重量级锁)
2-4-2-2、使用notify
在偏向锁中添加notify
运行结果如下:
通过上图,obj的101(可偏向锁状态)-->通过synchronized加锁变更为偏向锁-->通过调用obj.notify--->000(轻量级锁)
2-4-2-3、小结
在偏向锁状态下,使用wait会启用monitor的重量级锁。使用notify会变成轻量级锁。
2-5、轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。
2-5-1、轻量级锁跟踪
@Slf4j
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
// 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
obj.hashCode();
//log.debug(ClassLayout.parseInstance(obj).toPrintable());
new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
},"thread1").start();
Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
}
运行结果
2-5-2、测试:锁升级场景
2-5-2-1、偏向锁升级轻量级锁
模拟两个线程轻微竞争场景
@Slf4j
public class LockEscalationDemo2 {
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
// 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
//obj.hashCode();
//log.debug(ClassLayout.parseInstance(obj).toPrintable());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName() + "开始执行。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
// 思考:偏向锁执行过程中,调用hashcode会发生什么?
//obj.hashCode();
log.debug(Thread.currentThread().getName() + "获取锁执行中。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName() + "释放锁。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
}
}, "thread1");
thread1.start();
//控制线程竞争时机
Thread.sleep(1);
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
},"thread2");
thread2.start();
Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
}
运行结果如下:
2-5-2-2、轻量级锁膨胀为重量级锁
@Slf4j
public class LockEscalationDemo3 {
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
},"thread1").start();
new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
+ClassLayout.parseInstance(obj).toPrintable());
}
},"thread2").start();
Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
}
运行结果如下:
2-6、总结:锁对象状态转换
1、不存在无锁-->偏向锁的转换
2、重量级锁为了避免park,会进行CAS自旋
3、偏向锁解锁的时候,不会变为无锁状态,而是变为可偏向锁(无线程ID)
4、偏向锁有轻微竞争会偏向锁撤销并升级为轻量级锁,轻量级锁解锁会变为无锁状态;偏向锁无锁状态,调用hashCode会变为无锁状态(偏向锁无法存hashcode,无锁状态可以存hashcode);偏向锁已锁定,同步代码块中调用hashcode或者wait会升级为重量级锁
偏向锁、无锁、轻量级锁、重量级锁的转换