一.Java对象内存布局和对象头
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header) 、实例数据 (Instance Data) 和 对文填充 (Padding)。
- 对象内部结构分为:对象头、实例数据、对齐填充(保证8个字节的倍数)。
- 对象头分为对象标记(markOop)和类元信息(klassOop),类元信息存储的是指向该对象类元数据(klass)的首地址。
对象头
在64位系统中,Mark Word占了8个字节,类型指针占了4个字节(采用了压缩指针,压缩前是8个)。
对象标记Mark Word
类元信息(又叫类型指针)
指向它的类元数据(方法区的Klass对象)的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
存放类的属性(Field)数据信息,包括父类的属性信息。
对齐填充
虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
这部分内存按8字节补充对齐。
二.Synchronized与锁升级
2.1为什么 JDK1.6 以前synchronized是重量级锁
什么是上下文操作
线程多,不断阻塞、唤醒,一次阻塞+唤醒操作需要两次上下文切换:
- 就是第一次由用户态切换到内核态,由内核将线程阻塞掉,将线程数据原本在寄存器或者cache运送到内存或队列中
- 第二次由用户态切换到内核态,由内核将线程唤醒,将线程数据从内存或者队列送到寄存器或cache中。如果频繁进行上下文切换,CPU就大多数消耗在内核数据共享中,影响了真正工作线程功能
上下文切换过高会导致CPU像个搬运工,频繁在寄存器和运行队列之间奔波 ,更多的时间花在了线程切换,而不是真正工作的线程上。直接的消耗包括CPU寄存器需要保存和加载,系统调度器的代码需要执行。间接消耗在于多核cache之间的共享数据。
因为监视器锁(monitor)依赖底层操作系统的原语 pthread_mutex_lock 实现,当多个线程同时调用这个函数的时候,会让每一个线程都切换到内核态,由内核协调哪个线程获取到锁,哪些线程无法获取到锁。获取锁成功的线程会被唤醒,获取锁失败的线程会被内核进行阻塞。所以它是一个重量级操作。操作系统实现线程之间的切换需要从用户态到内核态的转换,这种状态切换需要耗费处理器时间。
总之一句话,重量级锁是需要依靠操作系统来实现互斥锁的,这导致大量上下文切换,消耗大量CPU,影响性能。
Java SE 1.6开始,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级为偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
2.2偏向锁
Hotspot 的作者经过研究发现,大多数情况下:锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况,为了让线程获得锁的代价更低而引入了偏向锁,这个线程就是锁的偏向线程,它的出现是为了解决只有在一个线程执行同步时提高性能。
- 首先获取锁对象的 Markword,判断是否处于可偏向状态。(biased_lock=1、且 ThreadId 为空)
- 如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID 写入到 MarkWord
- 如果 cas 成功,那么 markword 就会变成这样。 表示已经获得了锁对象的偏向锁,接着执行同步代码块
- 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行
- 如果是已偏向状态,需要检查 markword 中存储的 ThreadID 是否等于当前线程的 ThreadID
- 如果相等,不需要再次获得锁,可直接执行同步代码块
- 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁
偏向锁撤销
偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:
- 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
- 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向 。
关闭偏向锁
偏向锁在 Java 6 和 Java 7 里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0
如果你确定应用程序里所有的锁通常情况下处于竞争状态,那么偏向锁就是多余的,此时使用偏向锁是得不到提升的,反而可能降低系统性能,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking
从 JDK 15 开始,偏向锁被官方标记为废弃状态
2.3轻量级锁
轻量级锁加锁
加锁的过程很奇妙:线程在执行同步代码块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Work 复制到锁记录中。接下来线程会尝试使用CAS操作将对象头中的Mark Word替换为指向锁记录的指针。如果成功则获取锁,失败则表示被占用,自旋重试。
轻量级锁解锁
轻量级锁解锁时,会使用原子性的CAS操作将栈帧中的 Mark Word替换回到对象头,如果成功则表示没有竞争。失败则表示存在竞争,锁膨胀为重量级锁。
经过上面的学习,我们知道了JVM底层几种锁的实现,下面总结了几种锁的优缺点: