Java 线程同步-01:Java对象内存布局和Monitor机制

4 阅读4分钟

前言

在并发编程的世界中,synchronized关键字是Java开发者最常用的同步工具。但你是否思考过:当一个对象被synchronized锁定时,JVM底层到底发生了什么?为什么对象能"记住"哪个线程持有它的锁?

这一切的秘密,都藏在Java对象的内存布局中:理解对象内存结构,是掌握Java并发编程的基石。在 Java 线程同步机制中,synchronized 是基于 Java 对象头和 Monitor 机制来实现的。

Java对象的三层结构图

在HotSpot JVM中,每个Java对象在堆内存中都包含三个主要部分:对象头、实例数据和对齐填充。对于线程同步而言,我们更多关注的是其中的对象头部分。

graph TB
    subgraph "Java对象内存布局"
        A["整体对象内存"] --> B[对象头]
        A --> C[实例数据]
        A --> D[对齐填充]
        
        B --> E["Mark Word (8字节)"]
        B --> F["Klass Pointer (4/8字节)"]
        B --> G["数组长度 (4字节)"]
        
        E --> H["无锁: 01"]
        E --> I["偏向锁: 01"]
        E --> J["轻量级锁: 00"]
        E --> K["重量级锁: 10"]
        E --> L["GC标记: 11"]
        
        C --> M[非静态字段]
        C --> N[父类字段]
        
        D --> O["填充到8的倍数"]
    end
    
    style A fill:#f0f8ff
    style B fill:#e6f7ff
    style C fill:#f0fff0
    style D fill:#fff0f5
    style E fill:#fffacd

实例数据和对齐填充:

实例数据比较好理解,就是这个对象本身的一些变量属性,这里不展开介绍。另外,这里的对齐填充,其实是将java对象大小填充到8的倍数,内存对齐的目的主要有两个:

  1. 提高存取效率:对齐的数据可以通过一次内存操作完成读取
  2. 硬件要求:某些CPU架构要求特定类型的数据必须对齐存储

如果不对齐,一个跨越两个内存字的数据需要两次读取操作,性能会显著下降。

Java 对象头

在线程同步中,Java对象头比较重要,因此单独开个章节进行介绍。

什么是Klass Pointer?

Klass Pointer指向存储在方法区(Metaspace)中的Klass对象。这个Klass对象包含了类的所有元数据:

graph TD
    subgraph "方法区 Metaspace"
        Klass["Klass对象"] --> Methods["方法表 vtable"]
        Klass --> Fields["字段表"]
        Klass --> ConstPool["常量池"]
        Klass --> SuperKlass["父类Klass"]
        Klass --> Interfaces["接口表"]
        Klass --> AccessFlags["访问标志"]
        Klass --> ClassLoader["类加载器引用"]
    end
    
    subgraph "堆内存"
        Object["Java对象实例"] --> Header["对象头"]
        Header --> KP["Klass Pointer"]
        KP --> Klass
    end
    
    subgraph "元数据用途"
        Methods --> Use1["支持多态<br/>动态绑定"]
        Fields --> Use2["字段访问<br/>反射操作"]
        ConstPool --> Use3["符号引用<br/>字面量池"]
        SuperKlass --> Use4["继承链<br/>类型检查"]
    end
    
    style Klass fill:#d1ecf1
    style Object fill:#f9f9f9

什么是数组长度

数组对象在对象头中有一个额外的数组长度字段,这是普通对象所没有的:

public class ArrayHeaderDemo {
    public static void main(String[] args) {
        // 不同类型的数组
        int[] intArray = new int[10];
        String[] strArray = new String[5];
        Object[][] multiArray = new Object[3][4];
        
        // 获取数组长度
        System.out.println("int[] length: " + intArray.length);  // 存储在数组头
        System.out.println("String[] length: " + strArray.length);
        System.out.println("Object[][] length: " + multiArray.length);
    }
}

数组长度字段是实现Java数组安全访问的关键:

flowchart TD
    subgraph "数组访问流程"
        Start["访问 array[index]"] --> Load["加载数组对象"]
        Load --> GetHeader["获取对象头"]
        GetHeader --> Extract["提取数组长度"]
        
        Extract --> Check{"0 ≤ index < length ?"}
        Check -->|是| Calculate["计算元素地址<br/>array_start + header + index × size"]
        Calculate --> Access["安全访问元素"]
        
        Check -->|否| Throw["抛出<br/>ArrayIndexOutOfBoundsException"]
    end
    
    style Check fill:#ffcccc
    style Access fill:#ccffcc
    style Throw fill:#ffcccc

什么是Mark Word

Mark Word(标记字段)是对象头中最重要也最复杂的部分,它记录了对象运行时的状态信息。Mark Word的结构会根据对象所处的不同状态而变化。Mark Word记录了以下关键信息:

public class MarkWordFunctions {
    public static void main(String[] args) throws Exception {
        Object obj = new Object();
        
        // 1. 哈希码存储
        int hashCode = obj.hashCode();  // 第一次调用时计算并存入Mark Word
        System.out.println("HashCode: " + hashCode);
        
        // 2. GC分代年龄
        // 对象每经历一次Minor GC,年龄加1,达到阈值(默认15)则晋升老年代
        
        // 3. 锁状态信息
        synchronized (obj) {
            System.out.println("Object is locked");
        }
        
        // 4. 偏向锁信息
        // 第一个获取锁的线程ID会记录在Mark Word中
    }
}

Mark Word在不同锁状态下有不同的位分配:

stateDiagram-v2
    [*] --> 无锁状态: 对象创建
    无锁状态 --> 可偏向状态: 启用偏向锁
    可偏向状态 --> 偏向锁状态: 第一个线程获取锁
    
    偏向锁状态 --> 无锁状态: 偏向锁撤销
    偏向锁状态 --> 轻量级锁: 其他线程竞争
    
    无锁状态 --> 轻量级锁: 多个线程竞争
    可偏向状态 --> 轻量级锁: 多个线程竞争
    
    轻量级锁 --> 无锁状态: 锁释放
    轻量级锁 --> 重量级锁: 竞争激烈
    
    无锁状态 --> 重量级锁: 直接竞争激烈
    重量级锁 --> 无锁状态: 锁释放
    
    无锁状态 --> GC标记: 可达性分析
    重量级锁 --> GC标记: 可达性分析
    GC标记 --> [*]: 对象被回收

其中涉及到几种不同状态的锁,对应的Mark Word的详细结构可以参考如下:

flowchart TD
    subgraph "Mark Word 64位结构详情"
        direction TB
        
        subgraph "🟢 无锁状态 | 锁标志: 01"
            NL["25bit: unused<br/>(未使用)"] --> 
            HC["31bit: identity_hashcode<br/>(对象哈希码)"] --> 
            NU["1bit: unused<br/>(未使用)"] --> 
            AGE["4bit: age<br/>(GC分代年龄)"] --> 
            BL["1bit: biased_lock: 0<br/>(偏向锁标志)"] --> 
            LOCK["2bit: lock: 01<br/>(锁标志位)"]
        end
        
        subgraph "🟡 偏向锁状态 | 锁标志: 01"
            TID["54bit: thread<br/>(持有偏向锁的线程ID)"] --> 
            EPOCH["2bit: epoch<br/>(偏向锁时间戳)"] --> 
            BU["1bit: unused<br/>(未使用)"] --> 
            BAGE["4bit: age<br/>(GC分代年龄)"] --> 
            BBL["1bit: biased_lock: 1<br/>(偏向锁标志)"] --> 
            BLOCK["2bit: lock: 01<br/>(锁标志位)"]
        end
        
        subgraph "🔵 轻量级锁状态 | 锁标志: 00"
            LOCKREC["62bit: ptr_to_lock_record<br/>(指向栈中锁记录的指针)"] --> 
            LLOCK["2bit: lock: 00<br/>(锁标志位)"]
        end
        
        subgraph "🔴 重量级锁状态 | 锁标志: 10"
            MONITOR["62bit: ptr_to_monitor<br/>(指向Monitor对象的指针)"] --> 
            HLOCK["2bit: lock: 10<br/>(锁标志位)"]
        end
        
        subgraph "⚫ GC标记状态 | 锁标志: 11"
            GCINFO["62bit: GC信息<br/>(回收相关信息)"] --> 
            GLOCK["2bit: lock: 11<br/>(锁标志位)"]
        end
    end
    
    style NL fill:#e1f5fe
    style HC fill:#bbdefb
    style AGE fill:#c8e6c9
    style BL fill:#fff3cd
    style LOCK fill:#f8d7da
    
    style TID fill:#d1c4e9
    style EPOCH fill:#b39ddb
    style BAGE fill:#c8e6c9
    style BBL fill:#fff3cd
    style BLOCK fill:#f8d7da
    
    style LOCKREC fill:#ffecb3
    style LLOCK fill:#f8d7da
    
    style MONITOR fill:#ffcdd2
    style HLOCK fill:#f8d7da
    
    style GCINFO fill:#cfd8dc
    style GLOCK fill:#f8d7da

Monitor机制

Monitor是一种同步原语(Synchronization Primitive),它提供了对共享资源的互斥访问机制。在Java中,每个对象都关联着一个隐式的Monitor。在HotSpot JVM中,Monitor是通过C++的ObjectMonitor类实现的:

// hotspot/src/share/vm/runtime/objectMonitor.hpp (简化版)
class ObjectMonitor {
public:
    // 关键字段
    void* volatile _owner;           // 当前持有Monitor的线程
    volatile intptr_t _recursions;   // 锁重入次数
    ObjectWaiter* volatile _EntryList;  // 等待锁的线程队列
    ObjectWaiter* volatile _WaitSet;    // 调用wait()等待的线程队列
    volatile int _count;             // 用于记录线程获取锁的次数
    
    // 方法
    void enter(Thread* self);       // 获取锁
    void exit(Thread* self);        // 释放锁
    void wait(jlong timeout, bool interruptable, TRAPS);  // 等待
    void notify(Thread* self);      // 通知一个
    void notifyAll(Thread* self);   // 通知所有
    
private:
    void AddWaiter(ObjectWaiter* waiter);    // 添加等待者
    void RemoveWaiter(ObjectWaiter* waiter); // 移除等待者
    void DequeueWaiter(ObjectWaiter* waiter); // 出队
};

Mark Word 有一个字段指向 monitor 对象。monitor 中记录了锁的持有线程,等待的线程队列等信息。每个对象都有一个锁和一个等待队列,其中有三个关键字段:

  • _owner 记录当前持有锁的线程
  • _EntryList 是一个队列,记录所有阻塞等待锁的线程
  • _WaitSet 也是一个队列,记录调用 wait() 方法并还未被通知的线程。

对应的内存结构图可以参考如下:

graph TB
    subgraph "Java对象(重量级锁状态)"
        Obj["Java对象实例"] --> Header["对象头"]
        Header --> MW["Mark Word"]
        MW -->|指向| MonitorPtr["ptr_to_monitor"]
    end
    
    subgraph "ObjectMonitor对象"
        MonitorPtr --> ObjectMonitor["ObjectMonitor实例"]
        
        ObjectMonitor --> Fields["关键字段:"]
        Fields --> Owner["_owner: Thread*<br/>持有锁的线程"]
        Fields --> Recursions["_recursions: int<br/>重入次数"]
        Fields --> EntryList["_EntryList: ObjectWaiter*<br/>等待锁队列"]
        Fields --> WaitSet["_WaitSet: ObjectWaiter*<br/>wait等待队列"]
        Fields --> Count["_count: int<br/>锁计数器"]
        
        Owner --> Thread1["线程A (运行中)"]
        EntryList --> Thread2["线程B (阻塞)"]
        EntryList --> Thread3["线程C (阻塞)"]
        WaitSet --> Thread4["线程D (等待)"]
        WaitSet --> Thread5["线程E (等待)"]
    end
    
    subgraph "线程状态"
        Thread1 --> State1["RUNNABLE<br/>持有锁,执行中"]
        Thread2 --> State2["BLOCKED<br/>等待获取锁"]
        Thread3 --> State3["BLOCKED<br/>等待获取锁"]
        Thread4 --> State4["WAITING<br/>调用了wait()"]
        Thread5 --> State5["WAITING<br/>调用了wait()"]
    end
    
    style ObjectMonitor fill:#fff0f0,stroke:#d9534f,stroke-width:2px
    style Owner fill:#c1e1c1
    style EntryList fill:#ffd8b2
    style WaitSet fill:#b3e0ff

Monitor的操作机制如下:

  • 多个线程竞争锁时,会先进入 EntryList 队列。竞争成功的线程被标记为 Owner。其他线程继续在此队列中阻塞等待。
  • 如果 Owner 线程调用 wait() 方法,则其释放对象锁并进入 WaitSet 中等待被唤醒。Owner 被置空,EntryList 中的线程再次竞争锁。
  • 如果 Owner 线程执行完了,便会释放锁,Owner 被置空,EntryList 中的线程再次竞争锁。
stateDiagram-v2
    [*] --> 空闲状态: Monitor创建
    
    空闲状态 --> 持有状态: 线程获取锁成功
    持有状态 --> 空闲状态: 线程释放锁
    
    持有状态 --> 等待状态: 持有锁线程调用wait()
    等待状态 --> 阻塞状态: 被notify()唤醒
    阻塞状态 --> 持有状态: 重新获取到锁
    
    空闲状态 --> 阻塞状态: 线程竞争锁失败
    
    note right of 空闲状态
        _owner = NULL
        _EntryList = 空
        _WaitSet = 空
    end note
    
    note left of 持有状态
        _owner = 当前线程
        _recursions ≥ 1
        线程处于RUNNABLE状态
    end note
    
    note right of 阻塞状态
        线程在_EntryList中
        状态为BLOCKED
        等待被唤醒竞争锁
    end note
    
    note left of 等待状态
        线程在_WaitSet中
        状态为WAITING/TIMED_WAITING
        等待被notify()
    end note