—— 对象内存布局 和 对象头
对象构成布局
1. 对象头
对象标记 Mark Word
- 哈希码
- GC 标记 & 次数
- GC 年龄 采用 4 位 bit 存储,最大为 15(1111),所以 MaxTenutingThreshold 参数(分代年龄)的参数默认值为 15
- 同步锁标记
- 偏向锁持有者
- 默认存储对象的 HashCode,分代年龄和锁标志位等信息;这些信息都是与对象自身定义无关的数据,所以 MarkWord 被设置成一个固定的数据结构,以便在极小的空间内存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是在运行期间 MarkWord 里存储的数据会随着锁标志位的变化而变化
类元信息(类型指针)
- 对象指向它的类元数据的指针(存在方法的对象模板),虚拟机通过这个指针来确定这个对象是哪个类的实例
在64位系统中,Mark Word 占了 8 个字节,类型指针占了 8 个字节,一共是 16 个字节(忽略压缩指针的影响);相当于 new 一个空对象
2. 实例数据
- 存放类的属性(Field) 数据信息,包括父类的属性信息
3.对齐填充
- 虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在,仅仅是为了字节对齐这部分内存按8字节补充对齐
- 为什么是 8 字节的整数倍?
- 方便 JVM 计数,计算机大都是 64 位处理器,能处理 8 个字节的数据,性能更高,处理更快
—— Synchronized 与 锁升级
阿里规约: 高并发时,同步调用应该去考量锁的性能耗损。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁
锁的升级过程: 无锁 》 偏向锁 》轻量级锁 》 重量级锁
前言
- Java 5 以前,只有 Synchronized,这是操作系统级别的重量级操作(需要用户态和内核态之间的切换)
- 为什么每个对象都可以成为一个锁?
- Java 对象是天生的 Monitor,每一个 Java 对象都有成为 Monitor 的潜质,因为在 Java 的设计中,每一个 Java 对象都自带一把内部锁 或者 Monitor 锁
- Monitor 的本质是依赖底层操作系统 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本极高
- Java 6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁 和 偏向锁
锁升级流程
synchronized 用的锁是存在 Java 对象头里的 Mark Word 中,锁升级功能主要依赖 Mark Word 中锁标志位 和 释放偏向锁标志位
锁指向
- 偏向锁: Mark Word 存储的是偏向的线程ID
- 轻量锁:Mark Word 存储的是指向线程栈中 Lock Record 的指针
- 重量锁:Mark Word 存储的是指向堆中的 Monitor 对象的指针
偏向锁
- 概念:单线程竞争,当线程 A 第一次竞争到锁时,通过操作修改 Mark Word 中 的偏向线程 ID 、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步
- 作用:当一段同步代码一直被同一个线程多次访问,由于只有一个线程,那么该线程在后续访问时便会自动获取锁
- 大多数情况下,多线程中的锁不仅不存在多线程竞争,还存在锁由同一个线程多次获得的情况,偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能
- 偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句,连 CAS 操作都不做,直接提高程序性能
- 参数说明:实际上偏向锁在 JDK1.6 之后是默认开启的,但是启动时间有延迟,所以需要添加参数
-XX:BiasedLockingStartupDelay=0
(关闭延迟),让其在程序启动时立刻启动 - 开启偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- 关闭偏向锁:
-XX:-UseBiasedLocking
(会跳级进入轻量锁) - 偏向锁撤销:
- 当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁
- 竞争线程尝试 CAS 更新对象头失败,会等待到全局安全点 (此时不会执行任何代码)撤销偏向锁
- 注意:Java 15 逐步废弃偏向锁,HotSpot 资源优化,开销较大
轻量级锁
- 定义:多线程竞争,但是任意时刻最多只有一个线程竞争;即不存在锁竞争太过激烈的情况,也就没有线程阻塞
- 主要作用: 有线程来参加 竞争,但是获取锁的冲突时间极短;本质就是自旋锁 CAS
- 获取: 轻量级锁是为了在线程近乎交替执行同步块时提高性能,在没有多线程竞争的前提下,通过 CAS 中减少重量级锁使用操作系统互斥量产生的性能消耗,先自旋,不行才升级阻塞;当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
- CAS 自旋到达一定程度和次数之后会升级到重量级锁(Java 6 之后)
- 自适应自旋锁的大致原理:线程如果自旋成功了,那下次自旋的最大次数会增加,因为 JVM 认为既然上次成功了,那么这一次也很大概率会成功;反之,如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免 CPU 空转
- 自适应意味着自旋的次数不是固定不变的,而是根据同一个锁上一次自旋的时间 或者 拥有锁线程的状态来决定
- 偏向锁 和 轻量锁 的区别和不同:
- 争夺轻量级锁失败时,自旋尝试抢占锁
- 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
重量级锁
- 有大量的线程参与锁的竞争,冲突性很高
- 原理: Java 中 synchronized 的重量级锁,是基于进入和退出 Monitor 对象实现的。在编译时会将同步块的开始位置插入 Monitor Enter 指令,在结束位置插入 Monitor Exit 指令。当线程执行到 Monitor Enter 指令时,会尝试获取对象所对应的 Monitor 所有权,如果获取到了,会在 Monitor 的 owner 中存放当前线程的 ID,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个 Monitor
锁升级后,hashcode 去哪了?
- 锁升级为轻量或 重量级锁后,Mark Word 中保存的分别是 线程栈帧里的锁记录指针 和 重量级锁指针,已经没有位置再保存 hashcode、GC 年龄
- 官方文档:一个对象如果计算过哈希码,就应该一致保持该值不变,否则很多依赖对象哈希码的API 可能存在出错风险。当一个对象以净计算过一致性哈希码后,它再也无法进入偏向锁状态了;而当一个对象当前正处于偏向锁的状态,又收到需要计算一致性哈希请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的 Monitor 类里有字段可以记录非加锁状态下的 Mark Word ,其中自然可以存储原来的哈希码
各个锁的优缺点
- 偏向锁: 适用于单线程,在不存在锁竞争的时候进入同步方法则使用偏向锁
- 轻量级锁: 使用于竞争较不激烈的情况,存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法执行时间很短的话,采用轻量级锁虽然会占用占用 CPU 资源但是相对比使用重量级锁还是更高效
- 重量级锁:适用于竞争激烈的情况,如果同步方法执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁
JIT 编译器对锁的优化
- JIT: Just In Time Complier,即时编译器
- 锁消除:JVM 使用 JIT 对锁进行优化, 基于逃逸分析,如果局部变量在运行过程中没有出现逃逸,监测到了共享数据没有竞争的锁,可将这些锁进行消除
- 锁粗化:将同步块的作用范围尽可能小,为了需要同步操作数量尽可能少,将多次上锁解锁的请求合并为一次同步请求