对象头
对象在jvm中是如何存储的
在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分 为三个区域:对象头(Header)、实例数据(Instance Data)、对 齐填充(Padding)
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass
,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc
对象,这个对象中包含了对象头以及实例数据。
这里提到的对象头到底是什么呢?
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
}
上面代码中的_mark
和_metadata
其实就是对象头的定义。
本文展开介绍一下_mark
,即mark word。
对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
Mark word 记录了对象和锁有关的信息,当某个对象被 synchronized关键字当成同步锁时,那么围绕这个锁的一 系列操作都和Mark word有关系。
对markword的设计方式上,非常像网络协议报文头:将mark word划分为多个比特位区间,并在不同的对象状态下赋予比特位不同的含义。下图描述了在32位虚拟机上,在对象不同状态时 mark word各个比特位区间的含义。
同样,在HotSpot的源码中我们可以找到关于对象头对象的定义,会一一印证上图的描述。对应与markOop.hpp类。
enum { age_bits = 4,
lock_bits = 2,
biased_lock_bits = 1,
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2
};
从上面的枚举定义中可以看出,对象头中主要包含了GC分代年龄、锁状态标记、哈希码、epoch等信息。
从上图中可以看出,对象的状态一共有五种,分别是无锁态、轻量级锁、重量级锁、GC标记和偏向锁。在32位的虚拟机中有两个Bits是用来存储锁的标记为的,但是我们都知道,两个bits最多只能表示四种状态:00、01、10、11,那么第五种状态如何表示呢 ,就要额外依赖1Bit的空间,使用0和1来区分。
在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,表示非偏向锁。
为什么任何对象都可以实现锁 ?
1. 首先,Java 中的每个对象都派生自 Object 类,而每个Java Object 在 JVM 内部都有一个 native 的 C++对象 oop/oopDesc进行对应。 2. 线程在获取锁的时候,实际上就是获得一个监视器对象 (monitor) ,monitor 可以认为是一个同步对象,所有的 Java 对象是天生携带 monitor。在 hotspot 源码的 markOop.hpp文件中,可以看到下面这段代码。
多个线程访问同步代码块时,相当于去争抢对象监视器 修改对象中的锁标识,上面的代码中ObjectMonitor这个 对象和线程争抢锁的逻辑有密切的关系
synchroinzed的优化
1 减少锁住的范围
2 java 1.6之后 进行了锁优化 引入4种锁状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态
synchroinzed(lock){
//同步代码块
}
假如有两个线程ThreadA/ThreadB
- 只有ThreadA去访问 (大部分情况是属于这种)--》引入偏向锁 --》对象头 的Mark Word里 记录了 ThreadA的ThreadID、偏向锁标记
- ThreadA和ThreadB 交替访问--》轻量级锁---》自旋
- 多个线程同时访问--》阻塞
偏向锁
大部分情况下,锁不仅仅不存在多线程竞争, 而是总是由同一个线程多次获得,为了让线程获取锁的代 价更低就引入了偏向锁的概念。怎么理解偏向锁呢? 当一个线程访问加了同步锁的代码块时,会在对象头中存 储当前线程的ID,后续这个线程进入和退出这段加了同步 锁的代码块时,不需要再次加锁和释放锁。而是直接比较 对象头里面是否存储了指向当前线程的偏向锁。如果相等 表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。
偏向锁的获取和撤销逻辑
1. 首先获取锁 对象的Markword,判断是否处于可偏向状 态。( biased_lock=1、且ThreadId为空)
2. 如果是可偏向状态,则通过CAS操作,把当前线程的ID 写入到MarkWord
a) 如果 cas 成功,那么 markword 就会变成这样。 表示已经获得了锁对象的偏向锁,接着执行同步代码 块
b) 如果 cas 失败,说明有其他线程已经获得了偏向锁, 这种情况说明当前锁存在竞争,需要撤销已获得偏向 锁的线程,并且把它持有的锁升级为轻量级锁(这个 操作需要等到全局安全点,也就是没有线程在执行字 节码)才能执行
3. 如果是已偏向状态,需要检查 markword 中存储的 ThreadID是否等于当前线程的ThreadID
a) 如果相等,不需要再次获得锁,可直接执行同步代码 块
b) 如果不相等,说明当前锁偏向于其他线程,需要撤销 偏向锁并升级到轻量级锁
偏向锁的撤销
偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为 偏向锁并不存在锁释放的概念) ,而是在获取偏向锁的过程 中,发现 cas 失败也就是存在线程竞争时,直接把被偏向 的锁对象升级到被加了轻量级锁的状态。
对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程 有两种情况:
1. 原获得偏向锁的线程如果已经退出了临界区,也就是同 步代码块执行完了,那么这个时候会把对象头设置成无 锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前 线程
2. 如果原获得偏向锁的线程的同步代码块还没执行完,处 于临界区之内,这个时候会把原获得偏向锁的线程升级 为轻量级锁后继续执行同步代码块
在我们的应用开发中,绝大部分情况下一定会存在 2 个以 上的线程竞争,那么如果开启偏向锁,反而会提升获取锁 的资源消耗。所以可以通过jvm参数 UseBiasedLocking 来设置开启或关闭偏向锁
流程图分析
轻量级锁
轻量级锁不停自旋的前提是 绝大多数线程在获取锁之后,在非常短的时间内会去释放锁
自旋会占用CPU资源,所以在指定的自旋次数之后(默认10次)或者根据自适应自旋锁情况,如果还没有获取到轻量级锁,锁会膨胀成重量级锁---》阻塞
自旋锁
轻量级锁在加锁过程中,用到了自旋锁 所谓自旋,就是指当有另外一个线程来竞争锁时,这个线 程会在原地循环等待,而不是把该线程给阻塞,直到那个 获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
注意,锁在原地循环的时候,是会消耗cpu的,就相当于 在执行一个啥也没有的for循环。 所以,轻量级锁适用于那些同步代码块执行的很快的场景, 这样,线程原地等待很短的时间就能够获得锁了。 自旋锁的使用,其实也是有一定的概率背景,在大部分同 步代码块执行的时间都是很短的。所以通过看似无异议的 循环反而能提升锁的性能。 但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而 会消耗CPU资源。默认情况下自旋的次数是10次, 可以通过 preBlockSpin来修改
在JDK1.6之后,引入了自适应自旋锁,自适应意味着自旋 的次数不是固定不变的,而是根据前一次在同一个锁上自 旋的时间以及锁的拥有者的状态来决定。 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并 且持有锁的线程正在运行中,那么虚拟机就会认为这次自 旋也是很有可能再次成功,进而它将允许自旋等待持续相 对更长的时间。如果对于某个锁,自旋很少成功获得过, 那在以后尝试获取这个锁时将可能省略掉自旋过程,直接 阻塞线程,避免浪费处理器资源
重量级锁
升级到重量级锁之后,没有获取锁的线程会被阻塞----》blocked 状态
使用对象锁 (监视器monitor)机制
https://blog.csdn.net/tengxvincent/article/details/82866962