揭秘Java世界中oop-klass模型奥秘之oop
OOP-Klass模型简介
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
前情回顾
在《揭秘Java程序能够运行的核心逻辑》系列文章中提到Java程序能够运行的四个核心逻辑核心之一就是oop-klass体系,在后续的《揭秘Java世界中oop-klass模型奥秘》系列文章中会介绍这部分的内容。
OOP-Klass模型简介
理解 OOP-Klass 模型 是深入掌握 Java 对象内存布局、多态实现以及元空间(Metaspace)的基础。
这个模型的设计初衷是为了实现“动静分离”:将 Java 对象实例数据(OOP)与描述对象类型的元数据(Klass)分开存储,以提高内存利用率和垃圾回收效率。
通过以下几个核心维度来介绍相关知识。
-
OOP 体系(Ordinary Object Pointers)
- 重点:研究
hotspot/src/share/vm/oops/oop.hpp及其派生类(如instanceOop)。 - 核心逻辑:了解 Java 对象在堆中的表示方式,包括对象头(Mark Word 和 元数据指针)以及实例数据的排列。
- 重点:研究
-
Klass 体系(Metadata)
- 重点:研究
hotspot/src/share/vm/oops/klass.hpp及其子类(如InstanceKlass)。 - 核心逻辑:探索 JVM 如何在元空间中表示一个 Java 类,包括虚函数表(vtable)、字段布局、以及方法信息的存储。
- 重点:研究
-
Handle 与 内存屏障
- 重点:研究
hotspot/src/share/vm/runtime/handles.hpp。 - 核心逻辑:了解在 JVM 内部 C++ 代码执行过程中,如何通过 Handle 机制安全地引用可能被 GC 移动的 OOP 对象。
- 重点:研究
对象在内存里长什么样
在HotSpot 虚拟机中,Java 对象在内存中的布局分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。深入理解对象头(Object Header)是进行性能调优和并发编程的基础。深入理解对象头(Object Header)是进行性能调优和并发编程的基础。
深入解析对象头
下面结合 hotspot\src\share\vm\oops\oop.hpp 等源码文件,深入解析对象头的组成部分。
1. 对象头的整体结构
在 HotSpot 中,hotspot\src\share\vm\oops\oop.hpp中对象头的基类定义在 oopDesc 类中:
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark; // Mark Word
union _metadata { // Klass Pointer
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
// ...
};
对象头主要由两部分组成(如果是数组,则有三部分):
- Mark Word(标记字段):存储对象自身的运行时数据。
- Klass Pointer(类型指针):指向对象的类元数据。
- Array Length(数组长度):仅当对象是数组时存在,占用 4 字节。
2. Mark Word(标记字段)
Mark Word 是对象头中最复杂的部分。为了节省空间,它被设计成一个非固定的数据结构,根据对象的状态重用存储空间。
其具体定义在 hotspot\src\share\vm\oops\oop.hpp 中。以 64 位虚拟机为例,其 64 位的分布如下表所示:
Mark Word 的状态分布(64位)
| 状态 | 25 bit | 31 bit | 1 bit (unused) | 4 bit (age) | 1 bit (biased) | 2 bit (lock) |
|---|---|---|---|---|---|---|
| 无锁 (Normal) | unused | identity_hashcode | unused | 分代年龄 | 0 | 01 |
| 偏向锁 (Biased) | Thread ID (54bit) | Epoch (2bit) | unused | 分代年龄 | 1 | 01 |
| 轻量级锁 (Lightweight) | 指向栈中锁记录 (Lock Record) 的指针 | 00 | ||||
| 重量级锁 (Heavyweight) | 指向互斥量 (Monitor) 的指针 | 10 | ||||
| GC 标记 (Marked) | 指向 CMS 或 G1 标记信息的指针 | 11 |
核心组成部分的作用:
- Identity Hashcode:对象的哈希码(延迟计算)。注意,如果对象被加锁,这个值会移动到
ObjectMonitor中。 - 分代年龄 (GC Age):占用 4 位,因此对象晋升老年代的最大年龄是 2 4 − 1 = 15 2^4 - 1 = 15 24−1=15。这也是
-XX:MaxTenuringThreshold最大值为 15 的原因。 - 偏向锁标志 (Biased Lock Flag):1 位,标记是否开启偏向锁。
- 锁标志位 (Lock Tag):2 位,区分当前对象处于哪种锁状态。
- 指针字段:在锁升级(轻量级、重量级)后,Mark Word 存储的是指向栈帧锁记录或堆外
ObjectMonitor的内存地址。
源码深度分析
1. markOop 的定义
在 hotspot\src\share\vm\oops\markOop.hpp 中,markOop 被定义为一个指向 uintptr_t 的伪指针类型。通过位掩码(Masks)和位移(Shifts)来操作数据。
// markOop.hpp 片段
class markOopDesc: public oopDesc {
private:
// 这里的 uintptr_t 在 64 位下是 uint64_t
uintptr_t value() const { return (uintptr_t) this; }
public:
// 锁状态位的掩码
enum { lock_mask = 3, // 11
lock_mask_in_place = 3,
biased_lock_mask = 4, // 100
age_mask = 15, // 1111
...
};
// 判断是否是偏向锁
bool has_bias_pattern() const {
return (mask_bits(value(), biased_lock_mask_in_place) == biased_lock_pattern);
}
};
2. 锁升级的过程
对象头在并发场景下的作用至关重要。HotSpot 利用 Mark Word 的位状态实现了锁的无感升级:
- 偏向锁 (Biased Locking):当一个线程访问同步块时,会在 Mark Word 中记录该线程 ID。后续该线程进入时只需比对 ID,无需 CAS 操作。
- 轻量级锁 (Lightweight Locking):当出现竞争,偏向锁撤销,JVM 在当前线程的栈帧中建立
Lock Record,并将对象头的 Mark Word 拷贝过去(Displaced Mark Word),然后通过 CAS 将对象头指向该记录。 - 重量级锁 (Heavyweight Locking):竞争进一步加剧,升级为重量级锁。Mark Word 指向
ObjectMonitor结构,未抢到锁的线程进入等待队列并挂起(涉及内核态切换)。
3. Klass Pointer(类型指针)
Klass Pointer 是对象指向它的类元数据(Klass 对象)的指针。JVM 通过这个指针确定该对象是哪个类的实例。
压缩指针 (Compressed OOPs)
在 64 位机器上,原生指针占用 8 字节。为了减少内存开销,HotSpot 引入了 UseCompressedClassPointers 参数(在 UseCompressedOops 开启时默认开启):
- 未开启压缩:Klass Pointer 占用 8 字节 (64 bit)。
- 开启压缩:Klass Pointer 占用 4 字节 (32 bit)。
JVM 通过基地址加偏移量的方式(Base + Offset << Shift)来寻找实际的 64 位地址,从而在 4 字节空间内支持高达 32GB 的内存寻址。
4. 数组长度 (Array Length)
如果对象是一个数组,对象头还会多出一块记录数组长度的区域。参见hotspot\src\share\vm\oops\arrayOop.hpp中源码。
class arrayOopDesc : public oopDesc {
// ...
static int length_offset_in_bytes() {
return UseCompressedClassPointers ? 8 : 12;
}
};
- 作用:Java 虚拟机必须知道数组的大小才能进行边界检查(ArrayIndexOutOfBoundsException)以及计算数组占用的总内存。
- 大小:固定为 4 字节 (32 bit)。这意味着 Java 数组的最大长度不能超过 2 31 − 1 2^{31} - 1 231−1。
5. 内存布局示例 (64位虚拟机)
假设有一个普通的 Object 实例,且开启了压缩指针:
- Mark Word: 8 字节。
- Klass Pointer: 4 字节(压缩后)。
- 对齐填充 (Padding): 4 字节(为了满足 8 字节对齐)。
- 总大小: 16 字节。
如果是 int[5] 数组:
- Mark Word: 8 字节。
- Klass Pointer: 4 字节。
- Array Length: 4 字节。
- 实例数据: 5 * 4 = 20 字节。
- Padding: 4 字节(凑齐 40 字节)。
- 总大小: 40 字节。
总结
Java 对象头是 JVM 实现高效内存管理和并发控制的灵魂所在:
- Mark Word 通过位压缩技术,在极小的空间内集成了 GC、锁同步和身份标识等关键数据。
- Klass Pointer 建立了实例与元数据之间的桥梁,支持了反射、多态(虚方法表查询)等特性。
- Array Length 保证了数组访问的安全性和内存分配的准确性。
这种紧凑的设计虽然增加了源码阅读的复杂度(需要频繁进行位运算),但极大地提升了 Java 程序在海量对象场景下的内存利用率和运行效率。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)