java源码剖析: 对象内存布局、JVM锁以及优化


http://www.cnblogs.com/dennyzhangdd/p/6734638.html

一、目录

1.启蒙知识预热:CAS原理+JVM对象头内存存储结构

2.JVM中锁优化:锁粗化、锁消除、偏向锁、轻量级锁、自旋锁。

3.总结:偏向锁、轻量级锁,重量级锁的优缺点。

 

二、启蒙知识预热

开启本文之前先介绍2个概念

2.1.cas操作

为了提高性能,JVM很多操作都依赖CAS实现,一种乐观锁的实现。本文锁优化中用到了CAS,故有必要先分析一下CAS的实现。

CAS:Compare and Swap。

JNI来完成CPU指令的操作:

unsafe.compareAndSwapInt(this, valueOffset, expect, update);

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

打开源码:openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp,如下图:0

os::is_MP()  这个是runtime/os.hpp,实际就是返回是否多处理器,源码如下:

 

如上面源代码所示(看第一个int参数即可),LOCK_IF_MP:会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

 

2.2.对象头

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 本文只讲对象头。 

HotSpot虚拟机的对象头(Object Header)包括两部分信息:

第一部分"Mark Word":用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等.

第二部分"类型指针":对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 ) 

32位的HotSpot虚拟机对象头存储结构:(下图摘自网络) 

 

                                                              图1 32位的HotSpot虚拟机对象头

 

为了证实上图的正确性,这里我们看openJDK--》hotspot源码markOop.hpp,虚拟机对象头存储结构:

                                             图2 HotSpot源码markOop.hpp中注释

源码中对锁标志位这样枚举

1 enum {   locked_value             = 0,//00 轻量级锁
2          unlocked_value           = 1,//01 无锁
3          monitor_value            = 2,//10 监视器锁,也叫膨胀锁,也叫重量级锁
4          marked_value             = 3,//11 GC标记
5          biased_lock_pattern      = 5 //101 偏向锁
6   };

下面是源码注释:

                                        图3 HotSpot源码markOop.hpp中锁标志位注释

看图3,不管是32/64位JVM,都是1bit偏向锁+2bit锁标志位。上面红框是偏向锁(一个是指向给定线程的显示偏向锁,一个是匿名偏向锁)对应枚举biased_lock_pattern,下面红框是4种请求头结构。分别对应上面的前4种枚举。我们甚至能看见锁标志11时,是GC的markSweep(标记清除算法)使用的。(这里就不再拓展了)

==================================================================

三、JVM中锁优化

大家都知道java中锁synchronized性能较差,线程会阻塞。

在jdk1.6中对锁的实现引入了大量的优化来减少锁操作的开销:

锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。

锁消除(Lock Elimination):通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。

轻量级锁(Lightweight Locking):这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。

偏向锁(Biased Locking):无锁竞争的情况下跳过轻量级锁,即不执行CAS原子指令。

适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁),进入到阻塞状态。

3.1.偏向锁

  按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作。
  简单的讲,就是在锁对象的对象头(开篇讲的对象头数据存储结构)中有个ThreaddId字段,这个字段如果是空的,第一次获取锁的时候,就将自身的ThreadId写入到锁的ThreadId字段内,将锁头内的是否偏向锁的状态位置1.这样下次获取锁的时候,直接检查ThreadId是否和自身线程Id一致,如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段。提高了效率。
注意:当锁有竞争关系的时候,需要解除偏向锁,进入轻量级锁。

每一个线程在准备获取共享资源时:

第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁”.跳过轻量级锁直接执行同步体。

获得偏向锁如下图:

 

3.2.轻量级锁和重量级锁

如上图所示:

第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。

第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord.

第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋.

第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败 第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己.

注意点:JVM加锁流程

偏向锁--》轻量级锁--》重量级锁

从左往右可以升级,从右往左不能降级

四.总结

本文重点介绍了JVM对Synchronized的优化,但竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比:

 

 

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度。

如果始终得不到锁竞争的线程使用自旋会消耗CPU。

追求响应时间。

同步块执行速度非常快。

重量级锁

线程竞争不使用自旋,不会消耗CPU。

线程阻塞,响应时间缓慢。

追求吞吐量。

同步块执行速度较长。

 

 

 

 

 

 

 

 

 

==========================

参考:

《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》第二版JDK1.7(本文JDK1.8,内容无冲突)

------------------ 本人实力有限,理解有

展开阅读全文

没有更多推荐了,返回首页