我看 JAVA 之 线程同步(下)
我看 JAVA 之 线程同步(下)
对象内存存储
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头
对象头包含 Mark Word、Class Pointer 和 Array Length。
Mark Word
用于存储程序运行时的标志位,如锁状态、hashcode 和 GC 分代年龄等。
这部分数据的长度为 1 个 Word(字宽),在 32 位虚拟机中,一字宽等于 4 字节,即 32bit,在 64 位的虚拟机(暂不考虑开启压缩指针的场景)中一字宽等于 8 字节,即 64 个 Bits。
注:轻量级锁和偏向锁是 Java 6 对 synchronized 锁进行优化后新增加
Class Pointer
Class Pointer 指向实例对象对应的类信息的内存地址。
在 64 位系统下,这部分占 8 字节,开启压缩后占 4 字节;在 32 位系统下,占 4 字节。
数组长度
Array Length 为数组对象特有,其他对象不存。
实例数据
boolean 和 byte 占 1 字节
char 和 short 占 2 字节
int 和 float 占 4 字节
double 和 long 占 8 字节
对象引用 reference 8 字节
对齐填充
对齐填充仅起着占位符的作用,并不必须存在。 HotSpot 虚拟机要求任何对象的大小都必须是 8 字节的整数倍。
锁优化
在 我看 JAVA 之 线程同步(上) 我们简单介绍了管程 Monitor 以及 synchronized 关键字,了解了线程同步的基本概念。
如下代码为 objectMonitor.hpp 原文件中截取:
在 Java 早期版本中,synchronized 属于重量级锁,效率低下,而管程(monitor)是使用 Mutex Lock 实现的,存在从用户态到内核态的切换,代价是非常昂贵的;然而在大部分情况下,同步方法是运行在单线程环境,如果每次都调用 Mutex Lock 将严重的影响程序的性能。在 jdk1.6 中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。随着锁的竞争,出现无锁-->偏向锁-->轻量级锁-->重量级锁的升级,且锁的升级是单向的,只能从低到高升级,不会出现锁的降级。
偏向锁
当一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 的结构变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,避免在锁获取过程中执行不必要的 CAS 原子指令,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。CAS 原子指令虽然相对于重量级锁来说开销比较小但还是存在一定的本地延迟。
轻量级锁
如果倘若偏向锁失败,JVM 并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6 之后加入的),此时 Mark Word 的结构变为轻量级锁的结构。轻量级锁能够提升程序性能的前提是假设“对绝大部分的锁,在整个同步周期内都不存在竞争”,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场景,就会导致轻量级锁升级为重量级锁。
锁粗化(Lock Coarsening)
也就是减少不必要的紧连在一起的 unlock,lock 操作,将多个连续的锁扩展成一个范围更大的锁。
锁消除(Lock Elimination)
通过运行时 JIT 编译器的逃逸分析来消除一些不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,降低系统开销。
适应性自旋(Adaptive Spinning)
当线程在获取轻量级锁的过程中执行 CAS 操作失败时,会进入忙等待然后再次尝试(重试占用 cpu 资源),当尝试一定的次数后如果仍然没有成功,则进入到该 monitor 的_EntryList,变为阻塞状态。
评论