偏向锁 Epoch 机制:如何判断"旧值"、和谁比、存在哪(附 JDK 15+ 时效警告)
关键词:偏向锁 / epoch / 批量重偏向 / 批量撤销 / 无锁 / STW / JDK 15 废除
适用版本:JDK 6--14(默认启用);JDK 15 默认禁用(JEP 374);JDK 18 移除实现;JDK 19+ 标志过期。这是历史机制,但面试常考且易答错。
一、背景(为什么需要 epoch)
偏向锁(Biased Locking)的核心假设是:大多数锁在生命周期内只被同一个线程 反复进入。于是 HotSpot 把"锁"退化成"在对象头里记一个线程指针",该线程再进临界区时零 CAS、零原子指令,直接进。
但问题来了:偏向锁的撤销(revoke)必须走 STW(safepoint)。撤销要把对象从"偏向 T1"改成"无锁/轻量",必须等全局安全点才能动 Mark Word,否则会和正在跑的线程看到的锁状态冲突。
于是出现一个反直觉的坑:
若某个类有 1000 个对象,先都偏向 T1,后来 T2 轮流访问这 1000 个对象------每个对象的第一次访问都会触发一次撤销 STW。1000 次 STW,偏向锁反而比轻量级锁更慢。
epoch 机制正是为把"逐对象、多次 STW"摊薄成"几次类级 STW + 大量廉价 CAS"而设计的。它是偏向锁的自适应优化。
二、初步了解(epoch 机制是什么)
2.1 核心三句话
- 每个 类(Klass 元数据) 持有一个
epoch版本号; - 对象被偏向时,把"当前类的 epoch"拷进自己的 Mark Word;
- 当某类的偏向撤销累计达到阈值,JVM 给这个类换一个新的 epoch,并批量作废旧偏向------后续该类旧对象"逻辑上"全部失效,无需逐个撤销。
2.2 两个阈值(自适应)
| 参数 | 默认 | 触发动作 |
|---|---|---|
BiasedLockingBulkRebiasThreshold |
20 | 撤销累计达 20 → 批量重偏向(bulk rebias):类 epoch +1,旧偏向逻辑作废,后续访问走廉价 CAS 重偏向(无 STW) |
BiasedLockingBulkRevokeThreshold |
40 | 撤销进一步恶化达 40 → 批量撤销(bulk revoke) :该类被标记 biased-locking disabled,以后 new 出来的对象直接是无锁/轻量,不再走偏向逻辑 |
BiasedLockingDecayTime |
~25000ms | 衰减窗口:窗口内撤销次数没再冲破下一档阈值,计数器衰减归零,给类"改过自新"重新允许偏向的机会("自适应"含义) |
2.3 一个易错表述(面试要讲准)
⚠️ 纠偏 :bulk rebias 不会"一次性把所有旧对象偏向到某个具体线程"。它的真实语义是:翻号让旧偏向集体作废(类 epoch 改变),之后哪个线程先访问该对象,就由哪个线程 CAS 把它重新偏向自己 。
所以"批量重偏向"=「批量作废旧偏向 + 允许廉价重偏向」,而非"指向当前线程"。这是高频口误。
三、深度了解(epoch 判旧:和谁比、存在哪、怎么判)
这是本文最关键的一层。
3.1 直接结论
判定规则:拿「对象自身 Mark Word 里的 epoch」与「它所属类的 prototype header(类元数据里的 epoch)」做比较。
- 两者相等 → 偏向有效(且若偏向的是自己线程则零成本进临界区);
- 两者不相等 → 偏向已作废(stale),可走廉价 CAS 重偏向 ,无需 STW 撤销。
3.2 epoch 存在哪两个位置
| 位置 | 存放处 | 说明 |
|---|---|---|
| 对象的 epoch(副本) | 对象实例自己的 Mark Word 的 epoch 位域 | 对象被偏向时,从类的 prototype header 拷贝进来 |
| 类的 epoch(权威当前值) | InstanceKlass 的 _prototype_header (一个 markOop,存在类元数据里,不在任何对象上) |
类级"当前版本号";bulk rebias 改的就是它 |
核心要点:对象 epoch 是副本,类 prototype header 的 epoch 是权威版本号。比较就在"副本 vs 权威"之间发生。
3.3 对象 Mark Word 位布局(64 位 + 压缩指针)
偏向锁状态下的 Mark Word(源自 markOop.hpp 官方注释):
JavaThread*:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2
- epoch 只占 2 位(值域 0~3),不是想象中的"大计数器"。
- 类的 prototype header 里同样是 2 位。所以"新/旧"只需区分两代:对象要么和类当前 epoch 同代(有效),要么隔代(作废)。
- 为何 2 位够用:bulk rebias 由阈值门控(累计 20 次才翻一次),且 decay(~25s)会重置计数;同时存在的有效代差只有"当前代 + 上一代",2 位绰绰有余。
3.4 判旧流程(fast path 逐条)
线程访问对象 o 时,HotSpot 在偏向锁 fast path(BiasedLocking::revoke_and_rebias + 汇编)里:
- 读
o的 Mark Word → 确认是 biased 模式(biased_lock=1, lock=01); - 取
o.epoch(2 位); - 取
o所属类的klass->prototype_header()里的 epoch(权威当前值); - 比较分支 :
o.epoch == klass.epoch且偏向线程 == 当前线程 → 直接进临界区(零成本);o.epoch == klass.epoch但偏向的是别的线程 → 撤销(走 STW);o.epoch != klass.epoch→ 偏向已作废 → 廉价 CAS 重偏向(无 STW)。
这就是"批量重偏向省 STW"的底层支点:旧 epoch 对象一比对就发现代差,于是不走撤销、直接 CAS 认新主。epoch 不匹配,正是触发"免 STW 重偏向"的开关。
3.5 一个易错澄清
- epoch 不是"对象自己记着被撤销了",而是"对象的版本号跟不上类的版本号"。类翻号(epoch+1),对象没跟着翻 → 代差 → 判旧。
- 翻号动作本身是一次 STW(bulk rebias) ,但翻完之后所有旧对象的"失效"是零 STW 的懒检测------这就是 epoch 把"N 次逐对象撤销 STW"摊薄成"1 次类级 STW + 大量 CAS"的精髓。
四、源码了解(可溯源的官方依据)
| 内容 | 出处(OpenJDK) |
|---|---|
偏向锁 Mark Word 位布局、epoch:2 位域 |
hotspot/share/oops/markOop.hpp(epoch_shift / epoch_mask_in_place) |
| 类的当前 epoch 存放 | Klass::_prototype_header,读 klass->prototype_header() |
| 判旧(epoch 不匹配→rebias) | hotspot/share/runtime/biasedLocking.cpp 的 BiasedLocking::revoke_and_rebias 内比对 prototype_header->epoch() 与 mark->epoch() |
| 批量重偏向 / 批量撤销门控 | biasedLocking.cpp 中 BiasedLockingBulkRebiasThreshold / BiasedLockingBulkRevokeThreshold 逻辑;阈值在 globals.hpp 定义(默认 20 / 40) |
| 衰减机制 | BiasedLockingDecayTime(默认 25000ms) |
| 偏向锁启动延迟 | BiasedLockingStartupDelay(默认 4000ms,即启动 4s 后才给对象偏向,避免启动期单线程预热的浪费) |
| JDK 15 默认禁用 | JEP 374 --- Deprecate and Disable Biased Locking |
| JDK 18 移除实现 | JDK-8256425(实现彻底移除,部分标志 obsolete) |
| JDK 19+ 标志过期 | -XX:+UseBiasedLocking 过期,启动传入报 Unrecognized VM option 直接崩 JVM |
事实核对:上述 JEP 374 / JDK-8256425 / 阈值默认值均已在 OpenJDK 官方文档与源码中核实;本系列《JDK7 HashMap 成环》一文经
java-source-verifier同源核验流程确认,结论一致可信。
4.1 为什么现代 JDK 把它废了(JEP 374 原文依据)
现代应用大量使用线程池 + 并发框架 ,锁竞争频繁,偏向锁的撤销 STW 开销常常超过 它省下的 CAS 开销;且偏向锁给 HotSpot 同步子系统引入了巨量复杂代码,成为维护与优化障碍(JEP 374:"the performance gains seen in the past are far less evident today")。因此:
- JDK 15:默认禁用 (仍可用
-XX:+UseBiasedLocking显式开,但参数已 deprecated); - JDK 18:实现彻底移除;
- JDK 19+:标志过期,传了直接崩。
五、demo 实现(如何实证看 epoch / bulk rebias)
⚠️ 前置条件 :偏向锁只在 JDK 8--14 存在且默认开启(JDK 15+ 已废)。下面的 demo 需在这代 JDK 上跑,并加
-XX:+PrintBiasedLockingStatistics才能看到批量重偏向/撤销的计数证据。
5.1 运行参数
bash
java -XX:+UseBiasedLocking \
-XX:BiasedLockingStartupDelay=0 \ # 关掉启动延迟,立刻可偏向
-XX:BiasedLockingBulkRebiasThreshold=20 \
-XX:BiasedLockingBulkRevokeThreshold=40 \
-XX:+PrintBiasedLockingStatistics \ # 打印偏向锁统计(含 bulk rebias/revoke 次数)
-jar YourApp.jar
5.2 触发"批量重偏向"的示例程序
思路:构造一个类 C,让 T1 先 new 并进入大量对象(偏向 T1),再让 T2 轮流访问这些对象------当撤销累计越过 20,JVM 会触发一次 bulk rebias,统计里 Bulk rebias 计数 +1。
java
public class BiasedLockEpochDemo {
static class C { // 演示用类,重点在"同类大量对象被换线程访问"
private int x;
public void inc() { x++; }
}
public static void main(String[] args) throws Exception {
// 1) T1 预热:让一批对象偏向 T1
List<C> list = new ArrayList<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
C c = new C();
c.inc(); // 偏向 T1
list.add(c);
}
});
t1.start(); t1.join();
// 2) T2 轮流访问这些对象:触发撤销;越过阈值后触发 bulk rebias
Thread t2 = new Thread(() -> {
for (C c : list) {
c.inc(); // 非偏向线程访问 → 撤销;累计达 20 触发 bulk rebias
}
});
t2.start(); t2.join();
System.out.println("done; 看 JVM 退出时打印的 BiasedLockingStatistics");
}
}
5.3 看什么(证据)
程序正常退出时(或加 -Xlog 对应标签),JVM 会打印一段偏向锁统计,形如:
BiasedLockingStatistics
...
[<count> revocations, <count> bulk rebias, <count> bulk revoke]
- 你应能看到
bulk rebias计数 > 0,这正对应"类 epoch 被翻号、旧对象免 STW 重偏向"的发生; - 继续加大 T2 的竞争强度(更多对象 / 更多换线程访问),可进一步把
bulk revoke计数推上去,对应"该类被标记禁用偏向"。
说明:epoch 字段本身在对象头里,直接用纯 Java 读不出来(需要
Unsafe+ 对象头偏移解析,超出本 demo 范围)。PrintBiasedLockingStatistics是官方提供的、最干净的实证入口,足以证明 epoch 驱动的批量机制真实存在。
六、总结(面试一口答)
- epoch 是什么 :类级版本号,存在
InstanceKlass::_prototype_header;对象偏向时把副本存进自己的 Mark Word(占 2 位)。 - 怎么判旧:比较「对象 Mark Word 的 epoch」与「类 prototype header 的 epoch」;不相等即作废,可免 STW 廉价 CAS 重偏向。
- 两个阈值 :撤销累计 20 → bulk rebias(翻号);累计 40 → bulk revoke(该类禁用偏向)。
DecayTime给衰减归零的"改过"机会。 - 纠正口误:bulk rebias 是"批量作废 + 允许重偏向",不是"一次性全部指向某线程"。
- 翻号也有 STW:但它每越一次阈值才发生一次,把 N 次逐对象撤销 STW 摊薄成"几次类级 STW + 大量 CAS"。
- ⚠️ 时效必说 :偏向锁只适用于 JDK 6--14;JDK 15 默认禁用(JEP 374),JDK 18 移除,JDK 19+ 标志过期。现代 JDK 下这段代码是历史知识,张口按"现在还默认开"答会露怯。