前言

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

metaspace机制之空间结构的静态全景图

一、 Metaspace 核心架构静态全景图

在 OpenJDK 8 中,元空间(Metaspace)彻底替代了永久代(PermGen)。它的底层不是一块连续的内存,而是一套高度抽象的、由多级组件协同管理的级联式内存分配系统

1. 全景依赖拓扑网络

下面展示了元空间从 JVM 全局单例、类加载器实例到最底层操作系统匿名虚拟页(Anonymous Virtual Memory)的全景依赖模型。图中清晰区分了普通元数据区(Non-Class Space)与压缩类指针区(Class Space,即当 -XX:+UseCompressedClassPointers 开启时生效的物理隔离区)。

========================================================================================================================
                                             METASPACE STATIC ARCHITECTURE PANORAMA
========================================================================================================================

      [JVM Global State]                                                [Java ClassLoader Domain]
    +-----------------------+                                           +-----------------------+
    |  ClassLoaderDataGraph |                                           |    ClassLoaderData    |
    +-----------------------+                                           +-----------------------+
                |                                                                   |
                | (Iterates)                                                        | (Owns)
                v                                                                   v
    +-----------------------+                                           +-----------------------+
    |  ClassLoaderData N    | <---------------------------------------- |    Metaspace Instance |
    +-----------------------+     (Associated runtime mapping)          +-----------------------+
                                                                                    |
               +--------------------------------------------------------------------+
               | (Contains 1 or 2 SpaceManagers depending on CompressedClassPointers)
               v
    +-------------------------------------------------------------------------------------------------------------------+
    | SpaceManager 1: Non-Class Space (_vsm)               SpaceManager 2: Class Space (_class_vsm)                    |
    |                                                      (Active only if -XX:+UseCompressedClassPointers is true)      |
    |  - _mdtype = NonClassType                            - _mdtype = ClassType                                        |
    |  - _current_chunk -------------------------+         - _current_chunk --------------------------+                 |
    |  - _chunks_in_use (S/S/M/L Chunk Lists)    |         - _chunks_in_use (S/S/M/L Chunk Lists)     |                 |
    |  - _block_freelist (Manages wasted blocks) |         - _block_freelist (Manages wasted blocks)  |                 |
    +--------------------------------------------|----------------------------------------------------|-----------------+
                                                 |                                                    |
=================================================|====================================================|=================
                                                 | (Requests chunks from managers)                    |
                                                 v                                                    v
    +-------------------------------------------------------------------------------------------------------------------+
    | GLOBAL CHUNK MANAGEMENT LAYER                                                                                     |
    |                                                                                                                   |
    |  ChunkManager 1: Non-Class Zone Manager               ChunkManager 2: Class Zone Manager                          |
    |  +-------------------------------------------------+  +---------------------------------------------------------+ |
    |  | Free Lists by ChunkIndex:                       |  | Free Lists by ChunkIndex:                               | |
    |  |  - SpecializedChunkList (1KB / 2KB)            |  |  - SpecializedChunkList (1KB / 2KB)                    | |
    |  |  - SmallChunkList       (4KB / 8KB)            |  |  - SmallChunkList       (4KB / 8KB)                    | |
    |  |  - MediumChunkList      (64KB / 128KB)          |  |  - MediumChunkList      (64KB / 128KB)                  | |
    |  +-------------------------------------------------+  +---------------------------------------------------------+ |
    +-------------------------------------------------------------------------------------------------------------------+
                                                 |                                                    |
=================================================|====================================================|=================
                                                 | (Allocates memory chunks inside mapped segments)   |
                                                 v                                                    v
    +-------------------------------------------------------------------------------------------------------------------+
    | VIRTUAL SPACE ALLOCATION LAYER (OS Abstracted Virtual Memory Windows)                                             |
    |                                                                                                                   |
    |  VirtualSpaceList 1 (Non-Class Global Nodes)          VirtualSpaceList 2 (Class Global Nodes - Fixed 1GB Space)   |
    |  +-------------------+     +-------------------+      +---------------------------------------------------------+ |
    |  | VirtualSpaceNode  |---->| VirtualSpaceNode  |      | VirtualSpaceNode (Compressed Class Space Dedicated Node)| |
    |  | - ReservedSpace   |     | - ReservedSpace   |      | - ReservedSpace (Contiguous 1GB standard allocation)     | |
    |  | - VirtualSpace    |     | - VirtualSpace    |      | - VirtualSpace                                          | |
    |  | - _top游标        |     | - _top游标        |      | - _top游标                                              | |
    |  +-------------------+     +-------------------+      +---------------------------------------------------------+ |
    +-------------------------------------------------------------------------------------------------------------------+
               |                    |                                                            |
               +----------+---------+                                                            |
                          | (Slices memory block sequentially)                                   |
                          v                                                                      v
    +----------------------------------------------------+             +------------------------------------------------+
    | METACHUNK MEMORY LAYOUT (Slicing View)             |             | COMPRESSED METACHUNK LAYOUT                     |
    |                                                    |             |                                                |
    | +------------------------------------------------+ |             | +--------------------------------------------+ |
    | | Metachunk Header Area (Metadata, Next, Prev)   | |             | | Metachunk Header Area                      | |
    | +------------------------------------------------+ |             | +--------------------------------------------+ |
    | | Allocated MetaWord (InstanceKlass Oop Maps)     | |             | | Allocated MetaWord (CompressedInstanceKlass)| |
    | +------------------------------------------------+ |             | +--------------------------------------------+ |
    | | Allocated MetaWord (Method*)                   | |             | | Allocated MetaWord (CompressedConstantPool)| |
    | +------------------------------------------------+ |             | +--------------------------------------------+ |
    | | .............................................. | |             | | .......................................... | |
    | +------------------------------------------------+ |             | +--------------------------------------------+ |
    | | Free space pointed by _top cursor              | |             | | Free space pointed by _top cursor          | |
    | +------------------------------------------------+ |             | +--------------------------------------------+ |
    +----------------------------------------------------+             +------------------------------------------------+

2. 元空间核心组件语义与容量矩阵

为了在代码层建立清晰的映射,必须先明确各个物理与逻辑分层组件的核心职责。元空间没有像堆内存那样的 GC Roots 逐个对象扫描回收机制,它的空间完全依赖宿主组件进行生命周期强绑定。

组件名称 内存物理连续性 全局/线程域 最小/默认单元规格 (64位系统) 核心技术职责描述
VirtualSpaceList 逻辑连续(链表挂载) 全局单例 动态按需追加节点 管理 JVM 向操作系统通过 mmap 申请的所有底层 VirtualSpaceNode 的高阶链表结构。
VirtualSpaceNode 绝对物理连续 全局分配单元 默认非类区 2 MB 2\text{MB} 2MB,类区 1 GB 1\text{GB} 1GB 包装了系统的 ReservedSpace(预留虚拟地址空间)与 VirtualSpace(实际 Commit 的物理内存页),负责向更上层切出指定规格的 Metachunk。
ChunkManager 逻辑无序(按大小分类) 全局单例 维护 3 种主力 Chunk 空闲字典 负责全局死掉的(随着类加载器卸载而释放)Metachunk 的回收、暂存与二次复用管理,防止内存归还操作带来的系统调用高开销。
SpaceManager 逻辑无序(链表挂载) 类加载器私有 每个类加载器独占 1~2 个实例 真正执行内存配额控制的中枢。它从全局拿大块的 Metachunk,并用 Bump-the-pointer(游标碰撞)算法切分微观对象,管理当前加载器的分配步长(Step Sizing)。
Metachunk 绝对物理连续 单个类加载器独占 Specialized( 1 KB 1\text{KB} 1KB 2 KB 2\text{KB} 2KB)、Small( 4 KB 4\text{KB} 4KB 8 KB 8\text{KB} 8KB)、Medium( 64 KB 64\text{KB} 64KB 128 KB 128\text{KB} 128KB) 内存分配的物理原子宿主。它的头部包含元数据指针,尾部是纯净的、供 SpaceManager 动态切割的 MetaWord 数组。
MetaspaceObj 依据对象大小连续 逻辑实体 依具体元数据类型而定(如 Method、ConstantPool) 终极的运行时 C++ 内存实体,存储 Java 类的元结构信息。注意它绝不在 Java 堆中,属于 Native 进程常驻内存。

二、 OpenJDK 8 核心源码结构与深度注释

以下源码及注释基于 OpenJDK 8 分支中 hotspot/src/share/vm/memory/ 目录下的核心实现。为了展示资深系统工程师的深度审视,注释中重点剖析了字对齐(Word alignment)、多线程竞争状态、指针边界碰撞及底层页提交的边缘 case。

1. 终极数据物理原子载体:metachunk.hpp 与 metachunk.cpp

Metachunk 在物理上是一段连续的 native 内存。它不负责任何复杂的回收策略,内部只维护一个推进指针 _top。

// share/vm/memory/metachunk.hpp

class Metachunk : public Metabase<Metachunk> {
  friend class VMStructs;
  
private:
  // 属于哪一个底层的虚拟内存物理节点 (VirtualSpaceNode)
  VirtualSpaceNode* const _container;

  // 核心物理边界指针
  MetaWord* _top;          // 动态分配游标:指向当前 Chunk 内部未分配区域的起始首地址
  MetaWord* const _initial_top; // 当前 Chunk 物理空间的绝对起始地址 (紧随 Metachunk 头部结构体之后)
  MetaWord* const _end;    // 当前 Chunk 物理空间的绝对结束边界地址

  // 状态标记:表明当前 Chunk 是否已被分配给某个类加载器使用
  bool _is_tagged_free;

  // 内部空间大小统计(单位均为 Word,即 64 位系统下为 8 字节)
  size_t _word_size;

public:
  Metachunk(size_t word_size, VirtualSpaceNode* container);

  // 指针碰撞(Bump-the-pointer)核心高频函数
  inline MetaWord* allocate(size_t word_size) {
    // OpenJDK 8 的元空间对象分配必须进行字对齐检查,word_size 传入前已完成对齐
    if (_top + word_size <= _end) {
      MetaWord* result = _top;
      _top += word_size; // 游标高位推进,O(1) 复杂度
      return result;     // 返回分配给元数据对象 (如 Method*) 的物理首地址
    }
    return NULL;         // 空间不足,返回 NULL 触发 SpaceManager 向上级申请新 Chunk
  }

  // 基础属性访问器
  VirtualSpaceNode* container() const { return _container; }
  MetaWord* bottom() const { return (MetaWord*) this; }
  MetaWord* top() const { return _top; }
  MetaWord* end() const { return _end; }
  size_t word_size() const { return _word_size; }

  // 封存清空当前 Chunk(当它不再能容纳大对象时,将其残余空间转入 SpaceManager 的废弃块列表中)
  void set_top(MetaWord* v) { _top = v; }
  bool is_tagged_free() { return _is_tagged_free; }
  void set_is_tagged_free(bool v) { _is_tagged_free = v; }
};

// share/vm/memory/metachunk.cpp

Metachunk::Metachunk(size_t word_size, VirtualSpaceNode* container)
  : Metabase<Metachunk>(word_size),
    _container(container),
    _word_size(word_size),
    _is_tagged_free(false) {
  
  // 在物理内存切分时,Metachunk 的头部存放的是当前这个 Metachunk 的 C++ 实例本身
  // 真正的可用数据存储区(MetaWord 阵列)紧随其后
  _initial_top = (MetaWord*)this + Oscar::align_size_up(sizeof(Metachunk), BytesPerWord) / BytesPerWord;
  _top = _initial_top;
  _end = (MetaWord*)this + word_size;
  
  assert(_top <= _end, "Metachunk initialization error: overflow header layout");
}

2. 类加载器的专属私有分配代理:SpaceManager(在 metaspace.cpp 中定义)

SpaceManager 是非线程安全的,因为 Java 的类加载器在加载类时会有并发保护(或由类加载器专属锁保护)。它是连接微观对象和宏观物理内存页的关键纽带。

// share/vm/memory/metaspace.cpp

class SpaceManager : public CHeapObj<mtClass> {
  friend class Metaspace;

private:
  Metaspace::MetadataType _mdtype;   // 标识当前管理器负责 ClassSpace 还是 Non-Class Space
  AllocRecordList         _alloc_record_head; // 用于追踪分配历史的链表(在 Debug 模式下使用)

  // 当前类加载器拥有的物理块资产配置
  Metachunk* _current_chunk;         // 核心活跃块:当前所有 allocate 操作都在该块上通过指针碰撞完成
  
  // 按 ChunkIndex 类别归档的已用满/历史物理块链表(Specialized, Small, Medium, Large)
  Metachunk* _chunks_in_use[NumberOfInUseLists]; 
  
  // 块内部零散碎片复用字典:当一个元数据对象死亡或被替换(如重定义类),其占用的块内小空间
  // 会暂时扔到这个 FreeList 里面,下次分配相同大小对象时优先在这里找,防止“块内碎片化”
  BlockFreelist* _block_freelist;

  // 统计数据
  size_t _allocated_blocks_words;    // 逻辑分配出去的对象大小总和 (以 Word 为单位)
  size_t _allocated_chunks_words;    // 从全局申请到的 Metachunk 物理总大小

  Mutex* const _lock;                // 保护当前 SpaceManager 操作的轻量级锁(锁粒度在 ClassLoaderData 级别)

  // 动态计算下一次扩容时的 Metachunk 大小
  size_t calc_next_chunk_size(size_t word_size);

public:
  SpaceManager(Metaspace::MetadataType mdtype, Mutex* lock);
  ~SpaceManager();

  // 上层核心接口:分配细粒度元数据对象内存
  MetaWord* allocate(size_t word_size);
  
  // 扩容核心:申请并挂载新 Chunk,同时完成用户内存的切割分配
  MetaWord* grow_and_allocate(size_t word_size);

  // 清理:当类加载器被 GC 回收时,将自身持有的所有 Chunk 整体归还给全局复用池
  void retire_current_chunk();
};

// share/vm/memory/metaspace.cpp (核心实现部分)

MetaWord* SpaceManager::allocate(size_t word_size) {
  MutexLockerEx cl(lock(), Mutex::_no_safepoint_check_flag);

  // 所有的分配尺寸必须向上对齐到 MetaWordSize (64位下为 8 字节)
  size_t raw_word_size = Oscar::align_size_up(word_size, Metaspace::AllocationGranularity);
  MetaWord* result = NULL;

  // 步骤 1: 优先从垃圾碎片箱(BlockFreelist)中寻找以前废弃的、大小正合适的散落内存块
  if (_block_freelist != NULL && _block_freelist->total_size() > 0) {
    result = _block_freelist->get_block(raw_word_size);
    if (result != NULL) {
      _allocated_blocks_words += raw_word_size;
      return result;
    }
  }

  // 步骤 2: 尝试在当前活跃的 Metachunk 内部通过游标碰撞分配
  if (_current_chunk != NULL) {
    result = _current_chunk->allocate(raw_word_size);
  }

  // 步骤 3: 活跃块彻底满了(返回 NULL),必须触发全局扩容并更换新块
  if (result == NULL) {
    result = grow_and_allocate(raw_word_size);
  }

  if (result != NULL) {
    _allocated_blocks_words += raw_word_size;
  }
  return result;
}

MetaWord* SpaceManager::grow_and_allocate(size_t word_size) {
  // 步骤 3.1: 依据当前加载器的类型(如 BootstrapLoader、AppLoader)和历史分配曲线
  // 动态预测并计算出下一个最合理的 Chunk 规格 (如从 Small 升级到 Medium)
  size_t next_chunk_word_size = calc_next_chunk_size(word_size);

  // 步骤 3.2: 尝试去全局单例 ChunkManager 的空闲二级复用链表(FreeList)中“摸一模”有没有现成块
  Metachunk* next_chunk = chunk_manager()->chunk_freelist_allocate(next_chunk_word_size);

  // 步骤 3.3: 如果全局复用池干涸,说明进程第一次运行到该高位或内存吃紧,必须向一级物理页管理器申请硬切块
  if (next_chunk == NULL) {
    next_chunk = virtual_space_list()->get_new_chunk(next_chunk_word_size, _mdtype);
  }

  // 如果操作系统或者 JVM 限制(MaxMetaspaceSize)导致切不出任何新块,直接触发 OOM
  if (next_chunk == NULL) {
    return NULL;
  }

  // 步骤 3.4: 破旧迎新。将当前残存空间不够用的活跃块挂载到 _chunks_in_use 回收归档链表
  retire_current_chunk();

  // 步骤 3.5: 将新块设为当前主分配块,并更新账本
  _current_chunk = next_chunk;
  _allocated_chunks_words += next_chunk_word_size;

  // 步骤 3.6: 在全新的连续 Chunk 空间内进行第一次指针碰撞,此次必然成功
  return _current_chunk->allocate(word_size);
}

3. 全局物理页面的开拓者:VirtualSpaceNode 与 VirtualSpaceList

这部分源码处理 JVM 与操作系统的虚实转换。VirtualSpaceNode 对应的是操作系统层面的虚拟地址映射。在 OpenJDK 8 中,为了防止内存碎片,这里引入了动态高级物理页提交(Commit)机制。

// share/vm/memory/metaspace.cpp

class VirtualSpaceNode : public CHeapObj<mtClass> {
  friend class VirtualSpaceList;

private:
  ReservedSpace _rs;          // 调用 OS 接口(mmap/VirtualAlloc)预留的虚拟地址段(不占物理内存)
  VirtualSpace  _virtual_space;// 真正被 Commit(映射物理内存/交换区页)的虚拟空间封装
  MetaWord*     _top;         // 节点高位指针:代表当前 Node 已经切成 Chunk 分配出去了多少物理空间
  VirtualSpaceNode* _next;    // 挂载到全局 VirtualSpaceList 的单向链表指针

  // 跟踪当前 Node 的内存分配状态
  size_t _reserved_words;     // 总预留字数
  size_t _committed_words;    // 总提交物理页字数

public:
  VirtualSpaceNode(size_t byte_size);
  ~VirtualSpaceNode();

  // 从该物理节点上硬切下一个指定大小的 Metachunk
  Metachunk* get_chunk_vs(size_t chunk_word_size);

  // 物理页精准提交控制
  bool ensure_range_is_committed(char* start, size_t byte_size);
};

// share/vm/memory/metaspace.cpp (核心物理分配实现)

Metachunk* VirtualSpaceNode::get_chunk_vs(size_t chunk_word_size) {
  size_t chunk_byte_size = chunk_word_size * BytesPerWord;

  // 边界防御:如果当前 Node 剩余的纯净地址空间,不足以支撑起当前规格的 Chunk,则直接返回 NULL
  if ((char*)_top + chunk_byte_size > _virtual_space.high()) {
    return NULL;
  }

  // 确定当前待分配 Chunk 的物理起始边界
  char* chunk_limit = (char*)_top;

  // 【核心高能点】:OpenJDK 8 为了防止内存暴涨,采用了渐进式物理页提交。
  // 在切出 Chunk 之前,必须确保对应的操作系统物理页已经被真正 Commit(分配物理实质内存)
  if (!ensure_range_is_committed(chunk_limit, chunk_byte_size)) {
    return NULL; // 如果 OS 无法提供物理页(如 swap 空间不足或达到进程限制),切块失败
  }

  // 指针碰撞推进物理节点高位
  _top += chunk_word_size;

  // 原生 Native 指针强转:在取得的物理内存首地址上,调用 C++ Placement New 构造 Metachunk 对象头部
  Metachunk* result = ::new (chunk_limit) Metachunk(chunk_word_size, this);
  
  return result;
}

bool VirtualSpaceNode::ensure_range_is_committed(char* start, size_t byte_size) {
  // 检查请求的物理内存范围是否已经超出了当前已提交的边界
  if (start + byte_size <= _virtual_space.high_boundary()) {
    // 触发底层底层操作系统的物理页面映射逻辑(封装了 pwrite/mprotect/mmap 激活)
    bool success = _virtual_space.expand_by(byte_size);
    if (success) {
      _committed_words += byte_size / BytesPerWord;
    }
    return success;
  }
  return true;
}


三、 元空间微观内存动态分配演进全链路机制

为了完整体现系统工程师处理高并发、复杂级联状态的视角,以下梳理了当 JVM 内部执行类加载、向元空间申请内存时的动态决策与演进链路。

1. 动态分配与扩容全链路顺序演进

下述步骤详细拆解了当一个类加载器遭遇内存不足、最终触发底层操作系统物理页增发并完成指针碰撞的完整技术闭环:

  1. 1. 提出微观分配请求: SpaceManager 域.
    Java 运行时尝试加载类,提取出元数据对象并将其向上字对齐(Word alignment)。随后,它向专属的 SpaceManager::allocate() 发起目标尺寸(如 32 字节)的内存切分请求。

  2. 2. 散落块碎片箱检索: BlockFreelist 域.
    SpaceManager 优先检索其私有的 _block_freelist 碎片箱。如果箱内存在由于过往类重定义或元数据微调产生的、大小匹配的散落闲置块,则直接将其拦截复用并返回,避免浪费。

  3. 3. 活跃块指针碰撞: Metachunk 域.
    若碎片箱无匹配项,SpaceManager 定位到当前活跃的 _current_chunk,计算 _top + word_size 是否超过 _end。如果未越界,直接推进 _top 指针,返回原指针地址,分配宣告成功。

  4. 4. 升级扩容并求助于全局复用池: ChunkManager 域.
    若活跃块空间宣告枯竭,分配函数进入 grow_and_allocate。系统通过分配特征算法计算出下一个合理的 Chunk 升级尺寸(如由 Small 升级到 Medium),并发送给全局单例 ChunkManager,尝试从已死亡的类加载器遗留下来的空闲 FreeList 链表中捞取现成块。

  5. 5. 终极求助于物理操作系统的 anonymous 页: VirtualSpaceList 域.
    若全局 ChunkManager 同样空空如也,则向全局 VirtualSpaceList 发出底层硬切块请求。VirtualSpaceList 遍历或新建 VirtualSpaceNode,通过调用底层 ensure_range_is_committed 触发 mmap 系统调用,迫使操作系统提交、激活对应区间的物理内存页。

  6. 6. Placement New 头部重建与最终碰撞: 物理基址返回.
    在激活的物理地址首部通过 Placement New 构建全新的 Metachunk 对象。旧的、空间不足的 Chunk 被归档挂载到 _chunks_in_use 链表中。新 Chunk 被激活为 _current_chunk,并在其内部高频执行指针碰撞,最终将纯净的内存基地址返回给 JVM 运行时。


四、 核心架构设计之美:PermGen 与 Metaspace 的本质跨越

从系统工程师的视角来看,OpenJDK 8的 Metaspace 设计通过引入多级组件,完美解决了 PermGen 时代无法逾越的致命缺陷,实现了质的跨越:

核心维度 永久代(PermGen in JDK 7 及以前) 元空间(Metaspace in JDK 8) 系统工程学获益与深层设计考量
空间连续性与边界 连续的堆空间,由 -XX:MaxPermSize 强行指定硬上限。 离散的物理块(Chunk)组合,默认不设上限,直接利用本地进程的虚拟地址空间。 彻底规避了因为第三方框架(如 CGLIB、Spring AOP)动态大量生成代理类而导致的频繁 java.lang.OutOfMemoryError: PermGen space。
GC 与内存回收粒度 依赖老年代 GC 算法,必须通过扫描整个永久代的 GC Roots 进行逐个对象的死亡标记与存活迁移。 对象级别无 GC。实行“全面国有化,整体社会化”策略,与类加载器(ClassLoader)生命周期强绑定。 解耦了类元数据与 Java 堆 GC 的依赖。元空间不需要计算标记、不需要压缩拷贝。只有当 ClassLoader 本身被整体回收时,其持有的所有 Chunk 才会整块打包归还全局,垃圾回收效率达到 O ( 1 ) \mathcal{O}(1) O(1)
元数据物理隔离性 类的内部运行时实例(InstanceKlass)与类的常量池、方法字节码(Method)全部无差别混杂堆砌。 物理上彻底剥离。-XX:+UseCompressedClassPointers 开启后,仅将极其精简的类对象指针(32位)放在固定 1GB 的 Class Space 中,而庞大的方法体、方法数据、常量池全部挪到不受限的 Non-Class Space 中。 极大地精简了 64 位机器上的对象头(Object Header)尺寸(从 64 位压缩到 32 位),节省了宝贵的 CPU 缓存(L1/L2/L3 Cache)空间,同时让指针寻址速度维持在极高水准。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐