markword在高并发场景下变化剖析

markword在高并发场景下变化剖析

  • 前言
  • markword在高并发场景下变化剖析
    • [64位 markword的内存复用基准架构](#64位 markword的内存复用基准架构)
    • [第一次内存置换:轻量级锁的"栈帧置换"(Stack Displacement)](#第一次内存置换:轻量级锁的“栈帧置换”(Stack Displacement))
      • [1. 场景与触发时机](#1. 场景与触发时机)
      • [2. 底层置换机制](#2. 底层置换机制)
      • [3. OpenJDK 8核心源码解构](#3. OpenJDK 8核心源码解构)
    • [第二次内存置换:重量级锁膨胀的"堆/本地内存置换"(Monitor Inflation Displacement)](#第二次内存置换:重量级锁膨胀的“堆/本地内存置换”(Monitor Inflation Displacement))
      • [1. 场景与触发时机](#1. 场景与触发时机)
      • [2. 底层置换机制](#2. 底层置换机制)
      • [3. OpenJDK 8核心源码解构](#3. OpenJDK 8核心源码解构)
    • [第三次内存置换:GC 存活对象疏散的"转发指针置换"(GC Forwarding Displacement)](#第三次内存置换:GC 存活对象疏散的“转发指针置换”(GC Forwarding Displacement))
      • [1. 场景与触发时机](#1. 场景与触发时机)
      • [2. 底层置换机制](#2. 底层置换机制)
      • [3. OpenJDK 8核心源码解构](#3. OpenJDK 8核心源码解构)
    • 总结:系统视角下的三次内存置换精髓

前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。

markword在高并发场景下变化剖析

在 HotSpot 虚拟机(OpenJDK 8)中,每个 Java 对象头都包含一个关键的 Mark Word (在源码中体现为 markOop)。在 64 位架构下,markword占据 64 位的内存空间。由于这 64 位空间需要同时承载对象的哈希码、分代年龄、锁状态标志以及线程持有信息,JVM 设计了一种高度复用(Overloaded)的内存结构。

当对象生命周期发生演进或在高并发场景下遭遇激烈的锁竞争时,markword的常规数据结构会被强行打破,其内部原有的元数据(如 identity_hashcodeage)会被剥离并转移,腾出空间存放原生指针。这个过程在底层被称为 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_hashcodeage 的无锁 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 节点下:

cpp 复制代码
// share/vm/interpreter/bytecodeInterpreter.cpp

CASE(_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 Word
  markOop 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 方法中:

cpp 复制代码
// share/vm/runtime/synchronizer.cpp

ObjectMonitor* ATTR ObjectSynchronizer::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() ;
          return m ;
      }

      // 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 word
          markOop 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));
          return m ;
      }

      // 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) {
              ... // 失败处理
          }
          return m ;
      }
  }
}

第三次内存置换: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 方法中:

cpp 复制代码
// share/vm/oops/oop.inline.hpp

// 基础单线程/独占 GC 置换逻辑
inline void oopDesc::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 Pointer
  set_mark(mp);
}

// 多线程并行高并发 GC(如 G1/Parallel Scavenge)在实施对象搬迁竞争时的原子置换
inline oop 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
  // 目标置换为指向新地址的 mp
  markOop old = (markOop) Atomic::cmpxchg_ptr(mp, mark_addr(), compare);
  
  if (old == compare) {
    // 返回 NULL 代表置换成功:当前 GC 线程赢得了竞争,成功完成了对该对象的疏散和转发关联
    return NULL;
  } else {
    // 返回真实的 old 代表置换失败:说明另一个 GC 线程动作更快,已经把旧对象的 markword
    // 置换成了它复制的新对象地址。当前线程应放弃当前复制,去读取 old 里别人置换好的新对象地址
    return old;
  }
}

总结:系统视角下的三次内存置换精髓

从底层内存管理的本质来看,HotSpot 虚拟机的这三次 markword置换体现了极端紧凑的空间借调思想

  1. 轻量锁置换 是向当前线程的执行栈借用空间。它建立了一种"对象头指向栈,栈内保存原头"的父子双向绑定,用于在低竞争下快速识别锁归属。
  2. 重量锁置换 是向 Native Heap 堆内存 借用空间。它解除了与特定线程栈的物理绑定,将原对象头托付给全局的 ObjectMonitor,从而能够利用更复杂的等待队列(_WaitSet / _EntryList)和底层操作系统的 Mutex Lock 来应对高并发大厦将倾时的剧烈冲击。
  3. GC 转发置换 则是向未来的新内存对象借用空间。它直接将旧对象的生存尊严(markword空间)彻底剥夺,使其降级为一个纯粹的"路标指针"(Forwarding Pointer),以此在并发垃圾回收的洪流中,引导所有的存活引用完成地址的平滑迁移。
相关推荐
星夜夏空991 小时前
C++学习(1) ——C与C++
c语言·c++·学习
Cloud_Shy6181 小时前
Linux 用户管理知识与应用实践(二:用户相关命令与示例)
linux·运维·服务器·测试用例
旖-旎1 小时前
QT界面优化(6)
开发语言·c++·qt
小生不才yz1 小时前
Shell脚本精读 · S08-03 | 脚本模块化:`source` 与多文件组织
linux
想你依然心痛1 小时前
AtomCode在算法竞赛中的实战体验:LeetCode周赛辅助编程
linux·算法·leetcode
24计网1王仔寿1 小时前
Linux 系统运维全栈学习路线|从 Shell 脚本到容器云 OpenStack 完整学习指南
linux·学习·openstack
组合缺一1 小时前
用 ChatModel 构建 LLM 驱动的 Java 应用
java·开发语言·ai·llm·solon·rag
UP_Continue2 小时前
AutoCAD--图形命令和选项
c++·autopilot
vortex52 小时前
Shell 命令执行知识体系全景解析
linux·运维·bash·shell·命令行