前言
在并发编程的世界中,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的倍数,内存对齐的目的主要有两个:
- 提高存取效率:对齐的数据可以通过一次内存操作完成读取
- 硬件要求:某些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