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