diff --git a/jvm/深入理解java虚拟机.md b/jvm/深入理解java虚拟机.md index c98a31e..742eda4 100644 --- a/jvm/深入理解java虚拟机.md +++ b/jvm/深入理解java虚拟机.md @@ -77,6 +77,15 @@ - [符号引用](#符号引用) - [直接引用](#直接引用) - [初始化](#初始化-1) + - [锁优化](#锁优化) + - [自旋锁和自适应自旋](#自旋锁和自适应自旋) + - [锁消除](#锁消除) + - [锁粗化](#锁粗化) + - [轻量级锁](#轻量级锁) + - [Hotspot对象头内存布局](#hotspot对象头内存布局) + - [轻量级锁加锁过程](#轻量级锁加锁过程) + - [轻量级锁的解锁过程](#轻量级锁的解锁过程) + - [偏向锁](#偏向锁) # 深入理解java虚拟机 @@ -686,4 +695,91 @@ public static final int value = 123 ### 初始化 类初始化是类加载过程中的最后一步。在准备阶段,变量已经赋值过一次初始值,而在初始化阶段,`则会执行类构造器()方法`。 +## 锁优化 +### 自旋锁和自适应自旋 +在许多场景下,共享数据的锁定时间只会维持很短一段时间,为此挂起和恢复线程并不值得。对此,可以令后请求锁的线程`等待但并不放弃处理器的执行时间`,看看持有锁的线程是否会很快释放锁。为此,让线程执行`忙自旋`即可。 + +自旋锁并不能代替阻塞,自旋等待虽然避免了线程切换的开销,但是却要占用处理器时间: +- 如果锁被占用的时间很短,那么自旋等待的效果就较好 +- 如果锁被占用的时间很长,那么自旋线程只会白白浪费处理器资源 + +故而,自旋等待必须要有一定的限度,如果自旋超过限定次数仍然没有获取锁,就应当使用传统方式去挂起线程。自旋的默认次数为10次。 + +在jdk 1.6中,引入了自适应的自旋锁,自适应代表自旋时间不再固定了,而是由前一次在同一锁上的自旋时间以及锁拥有者的状态来决定: +- 如果在同一锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机会认为这次自旋也很有可能再次成功,其会允许自旋等待持续相对更长的时间,例如100个循环 +- 如果对于某个锁,自旋很少成功获得过,那么在以后获得这个锁时可能会省略自旋过程,以避免浪费处理器资源 + +拥有自适应自旋,随着程序的运行和性能监控信息的不断完善,虚拟机对程序锁状况的预测会越来越准确 + +### 锁消除 +在即时编译器运行时,对于`代码上要求同步,但经检测后不可能存在共享数据竞争`的锁进行消除。锁消除的主要判断依据为`逃逸分析`。如果判断在一段代码中,堆上所有的数据都不可能逃逸出去被其他线程访问到,那么可以将其看作是栈上数据,认为它们是线程私有的,自然无需进行同步加锁。 + +### 锁粗化 +原则上,在编写代码时,会倾向将同步块的作用范围限制的尽可能小,只在共享数据的实际作用域中才进行同步。这样,可以令需要同步的操作数量尽可能变少,如存在锁竞争,能够尽快拿到锁。 + +但是,如果存在一系列的连续操作都对同一个对象反复加锁/释放锁,甚至加锁操作出现在循环中,那么即使没有线程竞争,频繁的对锁进行获取和释放也会带来不必要的性能损耗。 + +故而,如果虚拟机检测到有这样一串零碎操作都对同一对象加锁,会将加锁的同步范围拓展(粗化)到整个操作序列外部。 + +### 轻量级锁 +在jdk 1.6中,加入了轻量级锁这一新型锁机制。`轻量级`是相对传统基于操作系统互斥量实现的锁来说的。 + +轻量级锁并非用于替代重量级锁,其用途是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量带来的性能消耗。 + +#### Hotspot对象头内存布局 +在Hotspot虚拟机对象头中包含两部分信息: +- 第一部分存储对象自身的运行时数据(Mark Word),如: + - hash码 + - gc分代年龄等 +- 存储指向方法区对象类型数据的指针 + +> 如果是数组类型,对象头中还会由额外部分用于存储数组长度 + +对象头信息是和对象自身定义数据无关的额外存储成本,为了考虑虚拟机空间效率,Mark Word被设计为了非固定的数据结构,以在尽量少的空间内存储更多信息。 + +Mark Word会根据对象状态复用自身的存储空间,例如在32位Hotspot虚拟机中: +- 如果对象处于未锁定状态下,Mark Word的32bit空间内容如下: + - 25bit用于存储对象的hash码 + - 4bit用于存储对象的分代年龄 + - 2bit用于存储锁标志位 + - 1bit固定为0 + +在其他状态下,Mark Word的存储内容如下所示: + +| 存储内容 | 标志位 | 状态 | +| :-: | :-: | :-: | +| 对象hash码和对象分代年龄 | 01 | 未锁定 | +| 指向锁记录的指针 | 00 | 轻量级锁定 | +| 指向重量级锁的指针 | 10 | 膨胀(重量级锁定) | +| 空,不需要记录信息 | 11 | GC标记 | +| 偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 | + +#### 轻量级锁加锁过程 +在代码进入同步块时,如果此时同步对象没有被锁定, +- 虚拟机将首先`在当前线程的栈帧中建立一个名为锁记录的空间`,用于存储对象目前的Mark Record拷贝。 +- 然后,虚拟机将会尝试使用CAS将对象的Mark Word更新为指向该锁记录的指针,`若更新成功,则线程则持有了该对象的锁`。 + - 之后,会将对象Mark Word的标志位改为00,代表对象处于轻量级锁定状态 +- 如果CAS更新Mark Word失败,虚拟机会检查Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了该锁,直接执行同步代码块 + - 如果Mark Word指向的不是当前线程的栈帧,那么代表该锁已经被其他线程抢先持有,此时,轻量级锁不再有效,需要膨胀为重量级锁,锁标记位会变为10,此时Mark Word中需要存储指向重量级锁(互斥量)的指针,后面等待的线程需要进入阻塞状态 + +#### 轻量级锁的解锁过程 +轻量级锁的解锁过程也是通过CAS来操作的 +- 如果Mark Word仍然指向线程的锁记录,那么会使用CAS将线程中复制的Mark Word替换回来,若替换成功,则整个解锁过程成功 +- 如果替换失败,说明有其他线程尝试获取过锁,那么需要在释放锁的同时唤醒被挂起的线程 + +轻量级锁仅在`整个同步周期内都不存在竞争`这一场景下能够提高性能。如果没有竞争,轻量级锁使用CAS规避了互斥量带来的开销。 + +但是,如果存在锁竞争,那么除了互斥量外,还额外发生了CAS操作,故而在存在锁竞争的场景下轻量级锁比重量级锁更慢。 + +### 偏向锁 +偏向锁也是jdk 1.6中引入的优化,其目的是进一步优化在无竞争情况下的程序性能。轻量级锁是在无竞争条件下用CAS操作去减少系统互斥量带来的开销;而偏向锁则是在无竞争条件下消除所有同步开销。 + +偏向锁代表其会偏向第一个获得其的线程,如果在后续执行的过程中,该所没有被其他线程获取,那么持有偏向锁的线程将永远不需要再同步。 + +如果虚拟机使用了偏向锁,那么在锁对象第一次被线程获取时,会将对象头中标志位设置为`01`,即偏向模式。同时,使用CAS操作将获取该锁的线程ID记录到对象的Mark Word中。若CAS成功,那么持有偏向锁的线程后续每次进入到该锁相关的同步代码中时,虚拟机都可以不再执行任何同步操作。 + +当有另一个线程尝试去获取该锁时,偏向模式将结束: +- 若当前对象处于未锁定状态,那么撤销偏向后,恢复到未锁定状态 +- 若当前对象处于已锁定状态,那么撤销偏向后,恢复到轻量级锁定状态 +