前言

本文旨在记录近期研读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;
  // ...
};

对象头主要由两部分组成(如果是数组,则有三部分):

  1. Mark Word(标记字段):存储对象自身的运行时数据。
  2. Klass Pointer(类型指针):指向对象的类元数据。
  3. 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 241=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 的位状态实现了锁的无感升级:

  1. 偏向锁 (Biased Locking):当一个线程访问同步块时,会在 Mark Word 中记录该线程 ID。后续该线程进入时只需比对 ID,无需 CAS 操作。
  2. 轻量级锁 (Lightweight Locking):当出现竞争,偏向锁撤销,JVM 在当前线程的栈帧中建立 Lock Record,并将对象头的 Mark Word 拷贝过去(Displaced Mark Word),然后通过 CAS 将对象头指向该记录。
  3. 重量级锁 (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 2311

5. 内存布局示例 (64位虚拟机)

假设有一个普通的 Object 实例,且开启了压缩指针:

  1. Mark Word: 8 字节。
  2. Klass Pointer: 4 字节(压缩后)。
  3. 对齐填充 (Padding): 4 字节(为了满足 8 字节对齐)。
  • 总大小: 16 字节。

如果是 int[5] 数组:

  1. Mark Word: 8 字节。
  2. Klass Pointer: 4 字节。
  3. Array Length: 4 字节。
  4. 实例数据: 5 * 4 = 20 字节。
  5. Padding: 4 字节(凑齐 40 字节)。
  • 总大小: 40 字节。

总结

Java 对象头是 JVM 实现高效内存管理和并发控制的灵魂所在:

  • Mark Word 通过位压缩技术,在极小的空间内集成了 GC、锁同步和身份标识等关键数据。
  • Klass Pointer 建立了实例与元数据之间的桥梁,支持了反射、多态(虚方法表查询)等特性。
  • Array Length 保证了数组访问的安全性和内存分配的准确性。

这种紧凑的设计虽然增加了源码阅读的复杂度(需要频繁进行位运算),但极大地提升了 Java 程序在海量对象场景下的内存利用率和运行效率。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐