markword在高并发场景下变化剖析 markword在高并发场景下变化剖析前言markword在高并发场景下变化剖析64位 markword的内存复用基准架构第一次内存置换轻量级锁的“栈帧置换”Stack Displacement1. 场景与触发时机2. 底层置换机制3. OpenJDK 8核心源码解构第二次内存置换重量级锁膨胀的“堆/本地内存置换”Monitor Inflation Displacement1. 场景与触发时机2. 底层置换机制3. OpenJDK 8核心源码解构第三次内存置换GC 存活对象疏散的“转发指针置换”GC Forwarding Displacement1. 场景与触发时机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_hashcode1位未使用4位age偏向锁 (Biased)54位Thread ID2位Epoch1位未使用4位age轻量级锁 (Lightweight)指向线程栈帧 Lock Record的原生指针— (00)00置换至当前线程栈重量级锁 (Heavyweight)指向Native Heap ObjectMonitor的原生指针— (10)10置换至 C 堆内存GC 转发 (Forwarded)指向To 空间/晋升目标空间新对象的原生指针— (11)11置换至新对象头部(原旧对象头报废)第一次内存置换轻量级锁的“栈帧置换”Stack Displacement1. 场景与触发时机当关闭偏向锁或者偏向锁由于多线程交替执行导致撤销后对象进入无锁状态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 lockeeSTACK_OBJECT(-1);CHECK_NULL(lockee);// 在当前线程栈帧中寻找一个空闲的锁记录 (Lock Record)BasicObjectLock*limitistate-monitor_base();BasicObjectLock*most_recent(BasicObjectLock*)istate-stack_base();BasicObjectLock*lockNULL;...// 获取对象当前最新状态的 Mark WordmarkOop marklockee-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 Displacement1. 场景与触发时机在轻量级锁状态下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 markobject-mark();assert(!mark-has_bias_pattern(),invariant);// CASE 1: 已经是重量级锁状态 (标志位为 10)说明其他线程完成了置换直接返回if(mark-has_monitor()){ObjectMonitor*mmark-monitor();returnm;}// CASE 2: 锁正在被其他线程实施膨胀中当前线程轻量级自旋等待其置换完成if(markmarkOopDesc::INFLATING()){ReadStableMark(object);continue;}// CASE 3: 当前是轻量级锁状态 (Stack-locked) —— 核心冲突高发区if(mark-has_locker()){// 【本地内存分配】从系统的 Native Heap 分配一个 ObjectMonitor 节点ObjectMonitor*momAlloc(Self);m-Recycle();m-_ResponsibleNULL;m-_recursions0;m-_spinDurationObjectMonitor::Knob_SpinLimit;// 【内存置换第一步】提取持有轻量级锁的线程栈中之前保存的 displaced mark wordmarkOop dmwmark-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 或 waitif(mark-is_neutral()){ObjectMonitor*momAlloc(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 Displacement1. 场景与触发时机在垃圾回收如 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// 底层会将其最后两位置换为 11markOop mpmarkOopDesc::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 mpmarkOopDesc::encode_pointer_as_mark(p);// 【并发原子置换】利用底层 CPU 提供的锁总线/缓存行指令进行 CAS// 期望旧对象头依然是 compare// 目标置换为指向新地址的 mpmarkOop old(markOop)Atomic::cmpxchg_ptr(mp,mark_addr(),compare);if(oldcompare){// 返回 NULL 代表置换成功当前 GC 线程赢得了竞争成功完成了对该对象的疏散和转发关联returnNULL;}else{// 返回真实的 old 代表置换失败说明另一个 GC 线程动作更快已经把旧对象的 markword// 置换成了它复制的新对象地址。当前线程应放弃当前复制去读取 old 里别人置换好的新对象地址returnold;}}总结系统视角下的三次内存置换精髓从底层内存管理的本质来看HotSpot 虚拟机的这三次 markword置换体现了极端紧凑的空间借调思想轻量锁置换是向当前线程的执行栈借用空间。它建立了一种“对象头指向栈栈内保存原头”的父子双向绑定用于在低竞争下快速识别锁归属。重量锁置换是向Native Heap 堆内存借用空间。它解除了与特定线程栈的物理绑定将原对象头托付给全局的ObjectMonitor从而能够利用更复杂的等待队列_WaitSet/_EntryList和底层操作系统的Mutex Lock来应对高并发大厦将倾时的剧烈冲击。GC 转发置换则是向未来的新内存对象借用空间。它直接将旧对象的生存尊严markword空间彻底剥夺使其降级为一个纯粹的“路标指针”Forwarding Pointer以此在并发垃圾回收的洪流中引导所有的存活引用完成地址的平滑迁移。