markword在高并发场景下变化剖析
markword在高并发场景下变化剖析
- 前言
- markword在高并发场景下变化剖析
- 64位 markword的内存复用基准架构
- 第一次内存置换:轻量级锁的“栈帧置换”(Stack Displacement)
- 1. 场景与触发时机
- 2. 底层置换机制
- 3. OpenJDK 8核心源码解构
- 第二次内存置换:重量级锁膨胀的“堆/本地内存置换”(Monitor Inflation Displacement)
- 1. 场景与触发时机
- 2. 底层置换机制
- 3. OpenJDK 8核心源码解构
- 第三次内存置换:GC 存活对象疏散的“转发指针置换”(GC Forwarding Displacement)
- 1. 场景与触发时机
- 2. 底层置换机制
- 3. OpenJDK 8核心源码解构
- 总结:系统视角下的三次内存置换精髓
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
markword在高并发场景下变化剖析
在 HotSpot 虚拟机(OpenJDK 8)中,每个 Java 对象头都包含一个关键的Mark Word(在源码中体现为markOop)。在 64 位架构下,markword占据 64 位的内存空间。由于这 64 位空间需要同时承载对象的哈希码、分代年龄、锁状态标志以及线程持有信息,JVM 设计了一种高度复用(Overloaded)的内存结构。
当对象生命周期发生演进或在高并发场景下遭遇激烈的锁竞争时,markword的常规数据结构会被强行打破,其内部原有的元数据(如identity_hashcode、age)会被剥离并转移,腾出空间存放原生指针。这个过程在底层被称为Displacement(置换)。
以下是 markword在底层生命周期与高并发下发生的三次最核心的“内存置换”。
64位 markword的内存复用基准架构
在理解置换之前,需明确其基础内存布局。通过下表可以直观看出,高 62 位在不同状态下承载的内容有着根本性的置换:
| 锁状态 (Lock State) | 高 62 位 (62 Bits Layout) | 偏向标志 (1 Bit) | 锁标志 (2 Bits) | 内存实际承载位置与说明 |
|---|---|---|---|---|
| 无锁 (Neutral) | 25位未使用 | 31位identity_hashcode | 1位未使用 | 4位age |
| 偏向锁 (Biased) | 54位Thread ID | 2位Epoch | 1位未使用 | 4位age |
| 轻量级锁 (Lightweight) | 指向线程栈帧 Lock Record的原生指针 | — (00) | 00 | 置换至当前线程栈 |
| 重量级锁 (Heavyweight) | 指向Native Heap ObjectMonitor的原生指针 | — (10) | 10 | 置换至 C++ 堆内存 |
| GC 转发 (Forwarded) | 指向To 空间/晋升目标空间新对象的原生指针 | — (11) | 11 | 置换至新对象头部(原旧对象头报废) |
第一次内存置换:轻量级锁的“栈帧置换”(Stack Displacement)
1. 场景与触发时机
当关闭偏向锁,或者偏向锁由于多线程交替执行导致撤销后,对象进入无锁状态(001)。此时,若有线程尝试进入同步块(synchronized),且当前无激烈竞争,JVM 会选择轻量级锁降低系统开销。
2. 底层置换机制
为了将整个 64 位的 markword空出来存放指向当前线程栈的指针,JVM 必须把对象当前包含identity_hashcode和age的无锁 markword备份。
- 写出:线程在自己的执行栈帧(Stack Frame)中分配一个
BasicObjectLock记录(即 Lock Record)。将对象头此时的无锁 markword拷贝到 Lock Record 内部的_displaced_header字段中。 - 写入:线程通过 CPU 的原子CAS (Compare-And-Swap)指令,尝试将对象头自身的 markword覆写为“指向该 Lock Record 的内存首地址”,并将锁标志位置换为
00。
3. OpenJDK 8核心源码解构
在解释器模式下,该置换动作的核心逻辑位于bytecodeInterpreter.cpp的_monitorenter节点下:
// share/vm/interpreter/bytecodeInterpreter.cppCASE(_monitorenter):{// 从操作数栈获取锁对象 (Oop)oop lockee=STACK_OBJECT(-1);CHECK_NULL(lockee);// 在当前线程栈帧中寻找一个空闲的锁记录 (Lock Record)BasicObjectLock*limit=istate->monitor_base();BasicObjectLock*most_recent=(BasicObjectLock*)istate->stack_base();BasicObjectLock*lock=NULL;...// 获取对象当前最新状态的 Mark WordmarkOop mark=lockee->mark();// 确认为非偏向锁且处于无锁(Neutral)状态if(mark->has_no_bias_in_cube()){// 【内存置换:第一步】将对象原有的 markword(包含哈希码、年龄) 暂存到栈帧锁记录的指定字段中// 官方命名十分直白:set_displaced_header(设置被置换的头部)lock->set_displaced_header(mark);// 【内存置换:第二步】通过原子 CAS 操作进行置换// 期望值:当前的 mark// 新值:指向当前栈帧 Lock Record 的指针 (最低两位由于内存对齐为 00)// 地址:lockee->mark_addr()if(Atomic::cmpxchg_ptr(lock,lockee->mark_addr(),mark)==mark){// 置换成功!意味着当前线程无视竞争,成功用栈指针替换了原对象头,获取了轻量级锁if(PrintBiasedLockingStatistics)return;}else{// CAS 失败,说明在置换期间别的线程插足了,触发锁升级/膨胀流程// CALL 慢速锁分配器...}}...}第二次内存置换:重量级锁膨胀的“堆/本地内存置换”(Monitor Inflation Displacement)
1. 场景与触发时机
在轻量级锁状态下(00),若发生多线程高并发激烈竞争(自旋失败),或者某个持有锁的线程调用了Object.wait()方法,锁就必须膨胀为依赖底层操作系统的重量级锁(10)。
2. 底层置换机制
升级为重量级锁意味着对象头必须再次让出全部空间,改为存放一个指向 C++ 堆内存中ObjectMonitor结构体的原生指针。
- 写出:锁膨胀器(Inflater)从 Native Heap 中分配或复用一个
ObjectMonitor。它必须读取当前正在持有轻量级锁的线程栈,将之前第一次置换时暂存在那里的displaced_header(无锁 Mark Word)取出来,再次置换并持久化到ObjectMonitor的_header字段中。 - 写入:利用 CAS 将对象的 markword设置为膨胀中标志
INFLATING锁定现场,接着最终改写为指向该ObjectMonitor的地址,并将标志位置换为10。
3. OpenJDK 8核心源码解构
该过程的核心逻辑位于synchronizer.cpp文件的ObjectSynchronizer::inflate方法中:
// share/vm/runtime/synchronizer.cppObjectMonitor*ATTRObjectSynchronizer::inflate(Thread*Self,oop object){// 保持自旋死循环,直到膨胀置换成功for(;;){markOop mark=object->mark();assert(!mark->has_bias_pattern(),"invariant");// CASE 1: 已经是重量级锁状态 (标志位为 10),说明其他线程完成了置换,直接返回if(mark->has_monitor()){ObjectMonitor*m=mark->monitor();returnm;}// CASE 2: 锁正在被其他线程实施膨胀中,当前线程轻量级自旋等待其置换完成if(mark==markOopDesc::INFLATING()){ReadStableMark(object);continue;}// CASE 3: 当前是轻量级锁状态 (Stack-locked) —— 核心冲突高发区if(mark->has_locker()){// 【本地内存分配】从系统的 Native Heap 分配一个 ObjectMonitor 节点ObjectMonitor*m=omAlloc(Self);m->Recycle();m->_Responsible=NULL;m->_recursions=0;m->_spinDuration=ObjectMonitor::Knob_SpinLimit;// 【内存置换:第一步】提取持有轻量级锁的线程栈中之前保存的 displaced mark wordmarkOop dmw=mark->displaced_mark_helper();// 【内存置换:第二步】将这个最初的无锁元数据,再次转移存储到 ObjectMonitor 的 _header 中m->set_header(dmw);// 设置重量级监视器的拥有者为原轻量级锁的 Lock Record 栈地址m->set_owner(mark->locker());m->set_object(object);// 【临界原子置换】通过 CAS 将对象头置换为特定的 INFLATING 状态标志if(Atomic::cmpxchg_ptr(markOopDesc::INFLATING(),object->mark_addr(),mark)!=mark){// 置换失败则释放申请的 Monitor 并重试omRelease(Self,m,true);continue;}// 【内存置换:第三步】最终收尾置换// 装配带有重量级锁标志(10)的 ObjectMonitor 原生指针,覆写到对象头中object->release_set_mark(markOopDesc::encode(m));returnm;}// CASE 4: 处于无锁状态下直接请求重量级锁(如直接调用了 hashcode 或 wait)if(mark->is_neutral()){ObjectMonitor*m=omAlloc(Self);m->Recycle();// 直接将当前的无锁 markword塞入 Monitor 的 _header 中m->set_header(mark);m->set_owner(NULL);m->set_object(object);...// 通过 CAS 彻底置换if(Atomic::cmpxchg_ptr(markOopDesc::encode(m),object->mark_addr(),mark)!=mark){...// 失败处理}returnm;}}}第三次内存置换:GC 存活对象疏散的“转发指针置换”(GC Forwarding Displacement)
1. 场景与触发时机
在垃圾回收(如 Minor GC、G1 的 Evacuation 阶段)发生时,GC 线程会扫描出所有的存活对象。为了解决内存碎片问题,GC 必须将这些存活对象搬迁(疏散)到新的内存区域(如 Survivor 空间或老年代)。
2. 底层置换机制
在高并发的多 GC 线程并行复制场景下,同一个旧对象可能同时被两个 GC 线程扫描到并尝试复制。为了确保一致性,并让所有指向旧对象的引用能够感知到新对象的存在:
- 写出:GC 线程在 To 空间分配一块新内存,将旧对象的全部内容(包含当前的 Mark Word)原封不动拷贝过去。
- 写入:随后,GC 线程利用 CAS 尝试强行将旧对象的 markword整体擦除替换。替换后的内容为:指向新对象内存首地址的原生指针,同时将其锁标志位硬编码置换为
11(已被 GC 标记转发)。 - 意义:后续其他引用指向旧对象时,只要读到 markword的最低两位是
11,就能立刻通过解引用该 markword中的Forwarding Pointer(转发指针)找到新对象。
3. OpenJDK 8核心源码解构
这个极度底层的并发置换逻辑存在于oop.inline.hpp对象的 inline 方法中:
// share/vm/oops/oop.inline.hpp// 基础单线程/独占 GC 置换逻辑inlinevoidoopDesc::forward_to(oop p){assert(Universe::heap()->is_in_reserved(p),"forwarding to something not in heap");// 【置换值构造】将新分配出来的对象内存首地址 p,编码转换成一个 markOop// 底层会将其最后两位置换为 '11'markOop mp=markOopDesc::encode_pointer_as_mark(p);assert(mp->decode_pointer()==p,"encoding must be reversible");// 【核心置换】直接覆写旧对象的 markword空间,将其变为 Forwarding Pointerset_mark(mp);}// 多线程并行高并发 GC(如 G1/Parallel Scavenge)在实施对象搬迁竞争时的原子置换inlineoop oopDesc::cas_forward_to(oop p,markOop compare){assert(Universe::heap()->is_in_reserved(p),"forwarding to something not in heap");// 将搬迁后的新对象首地址编码为带有 11 标志位的 markOop 转发指针markOop mp=markOopDesc::encode_pointer_as_mark(p);// 【并发原子置换】利用底层 CPU 提供的锁总线/缓存行指令进行 CAS// 期望旧对象头依然是 compare// 目标置换为指向新地址的 mpmarkOop old=(markOop)Atomic::cmpxchg_ptr(mp,mark_addr(),compare);if(old==compare){// 返回 NULL 代表置换成功:当前 GC 线程赢得了竞争,成功完成了对该对象的疏散和转发关联returnNULL;}else{// 返回真实的 old 代表置换失败:说明另一个 GC 线程动作更快,已经把旧对象的 markword// 置换成了它复制的新对象地址。当前线程应放弃当前复制,去读取 old 里别人置换好的新对象地址returnold;}}总结:系统视角下的三次内存置换精髓
从底层内存管理的本质来看,HotSpot 虚拟机的这三次 markword置换体现了极端紧凑的空间借调思想:
- 轻量锁置换是向当前线程的执行栈借用空间。它建立了一种“对象头指向栈,栈内保存原头”的父子双向绑定,用于在低竞争下快速识别锁归属。
- 重量锁置换是向Native Heap 堆内存借用空间。它解除了与特定线程栈的物理绑定,将原对象头托付给全局的
ObjectMonitor,从而能够利用更复杂的等待队列(_WaitSet/_EntryList)和底层操作系统的Mutex Lock来应对高并发大厦将倾时的剧烈冲击。 - GC 转发置换则是向未来的新内存对象借用空间。它直接将旧对象的生存尊严(markword空间)彻底剥夺,使其降级为一个纯粹的“路标指针”(Forwarding Pointer),以此在并发垃圾回收的洪流中,引导所有的存活引用完成地址的平滑迁移。