前言

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

metaspace的分配机制解析

在 OpenJDK 8中,Metaspace(元空间)的分配机制是一套高度优化的、基于类加载器生命周期的渐进式多层级内存分配系统。当 JVM 运行时需要为一个类分配元数据结构(如 InstanceKlassConstantPoolMethod 等)时,其底层会经历从“ClassLoader 逻辑空间 -> 全局块管理器 -> OS 虚拟内存节点”的级联分配链路。

以下结合 OpenJDK 8源码,深入剖析元空间分配机制的核心链路与底层核心源码。


一、 Metaspace 分配机制的核心链路

Metaspace 的内存分配链路是一个典型的自上而下、逐层兜底的倒金字塔模型。当一个 Java 类加载器尝试分配物理内存时,整体调用链条与决策逻辑如下:

 [上层触发分配]
  - 字节码解析、类加载或 JIT 编译
  - 调用 Metaspace::allocate(ClassLoaderData*, size_t, MetadataType)
         |
         v
 +---------------------------------------------------------------------------------------+
 | 1. ClassLoaderData 局部层 (Per-ClassLoader)                                            |
 |    - 根据 MetadataType (ClassZone / NonClassZone) 路由到对应的 SpaceManager               |
 |    - 进入 SpaceManager::allocate(size_t word_size)                                    |
 |         |                                                                             |
 |         +----> [当前 Metachunk 有空间] -> 调用 Metachunk::allocate() -------------------------> [成功: 游标指针碰撞返回]
 |         |                                                                             |
 |         +----> [当前 Metachunk 满/无块] -> 进入级联扩容 SpaceManager::grow_and_allocate()        |
 +---------------------------------------------------------------------------------------+
         |
         v
 +---------------------------------------------------------------------------------------+
 | 2. ChunkManager 全局缓存池层 (Global Free-List)                                         |
 |    - 根据所需尺寸计算目标 ChunkIndex (Specialized / Small / Medium / Humongous)          |
 |    - 调用 ChunkManager::chunk_freelist_allocate()                                      |
 |         |                                                                             |
 |         +----> [缓存池命中空闲块] -> 将块从 FreeList 摘除,绑定到 SpaceManager ---------------> [成功: 返回新 Chunk 碰撞分配]
 |         |                                                                             |
 |         +----> [未命中/无空闲块] -> 向下申请新块进入 VirtualSpaceList::get_new_chunk()            |
 +---------------------------------------------------------------------------------------+
         |
         v
 +---------------------------------------------------------------------------------------+
 | 3. VirtualSpaceNode 操作系统虚拟内存映射层 (OS Virtual Memory)                           |
 |    - 遍历全局 VirtualSpaceList 中的 VirtualSpaceNode 节点                                |
 |    - 进入 VirtualSpaceNode::get_chunk_vs(size_t chunk_word_size)                       |
 |         |                                                                             |
 |         +----> [当前 Node 剩余 Reserved 地址空间充足]                                   |
 |         |       - 调用 VirtualSpaceNode::commit_impl() 驱动 OS 映射物理页(mmap/commit)        |
 |         |       - 高位游标指针 _top 推进,切出物理块, Placement New 构造 Metachunk -----------> [成功: 返回给上层分配]
 |         |                                                                             |
 |         +----> [当前 Node 空间不足]                                                     |
 |                 - 创建并向 OS 申请一个新的大块虚拟内存节点 VirtualSpaceNode                  |
 |                 - 将新 Node 追加到全局链表,并从中切出物理块 ------------------------------------> [成功: 返回给上层分配]
 +---------------------------------------------------------------------------------------+
         |
         v
 [全局兜底防御与 OOM]
  - 若以上整条链路由于 MaxMetaspaceSize 限制或 OS 物理内存耗尽而失败
  - 触发垃圾回收器(GC)执行元空间引起的 Full GC(试图通过卸载 ClassLoader 来释放 Chunk)
  - GC 后再次尝试分配,若依旧失败,最终抛出 `java.lang.OutOfMemoryError: Metaspace`


二、 核心源码剖析与深度中文注释

以下根据上述核心分配链路的流动顺序,依次深入 OpenJDK 8的底层 C++ 源码实现,展现关键函数的控制逻辑与内存计算。

1. 入口层:Metaspace::allocate 路由

元空间分配的统一门面入口,负责根据是否开启压缩类指针(-XX:+UseCompressedClassPointers)来决定是将元数据放入非类区(NonClassZone)还是类区(ClassZone)

// hotspot/src/share/vm/memory/metaspace.cpp

MetaWord* Metaspace::allocate(ClassLoaderData* cld, size_t word_size,
                              MetadataType mdtype, TRAPS) {
  // 1. 防御性检查:如果是垃圾回收期间的无条件特殊分配或空请求,直接返回 NULL
  if (HAS_PENDING_EXCEPTION) {
    assert(false, "Cannot allocate with pending exception");
    return NULL;
  }
  if (word_size == 0) {
    return NULL;
  }

  // 2. 核心路由机制:如果开启了短指针压缩,且请求类型是类元数据(ClassType),则强制路由到专门的类空间
  // 类空间(Compressed Class Space)在底层有独立的虚拟空间基地址,用于支持 32 位紧凑寻址
  SpaceManager* sm = NULL;
  if (using_class_space() && mdtype == ClassType) {
    sm = cld->metaspace_non_null()->class_vsm();
  } else {
    sm = cld->metaspace_non_null()->vsm();
  }

  assert(sm != NULL, "SpaceManager should have been initialized");

  // 3. 将具体的分配任务下发给该 ClassLoader 专属的物理空间管理器(SpaceManager)
  MetaWord* result = sm->allocate(word_size);

  // 4. 极端兜底:如果分配失败(返回 NULL),说明元空间内存已满,需要触发分配失败的重试与垃圾回收机制
  if (result == NULL) {
    result = allocate_with_retry(cld, word_size, mdtype, TRAPS);
  }

  return result;
}

2. 专属空间层:SpaceManager::allocategrow_and_allocate

SpaceManager 负责管理当前类加载器已经申请到的所有 Chunk 链表。它执行高频的、线程安全的内存划分。

// hotspot/src/share/vm/memory/metaspace.cpp

MetaWord* SpaceManager::allocate(size_t word_size) {
  // 互斥锁控制:HotSpot 中,多个线程可能并发使用同一个 ClassLoader 加载不同的类(如应用类加载器)
  // 必须获取专属的锁(lock())来保证内部 Chunk 的游标移动是线程安全的
  MutexLockerEx cl(lock(), Mutex::_no_safepoint_check_flag);

  MetaWord* result = NULL;

  // 核心策略 1:尝试直接在当前激活的、立即可用的物理块(_current_chunk)中进行指针碰撞分配
  if (_current_chunk != NULL) {
    result = _current_chunk->allocate(word_size);
  }

  // 核心策略 2:如果当前块空间不足以容纳请求的大小,或者当前还没有任何物理块
  if (result == NULL) {
    // 触发局部的级联扩容与新块申请链条
    result = grow_and_allocate(word_size);
  }

  if (result != NULL) {
    // 成功分配后,递增当前加载器已分配的逻辑元数据块字数统计(用于 JMX 和 GC 阈值计算)
    _allocated_blocks_words += word_size;
  }

  return result;
}

// 当当前 Chunk 被填满时触发的核心扩容逻辑
Metachunk* SpaceManager::grow_and_allocate(size_t word_size) {
  // 1. 动态步长算法(Dynamic Sizing):
  // 根据当前类加载器的历史分配规模(例如是刚启动的临时加载器,还是核心的 SystemClassLoader),
  // 以及当前请求的目标字大小,计算出本次应该向系统申请什么尺寸档位的 Metachunk。
  size_t next_chunk_word_size = calc_next_chunk_size(word_size);
  ChunkIndex next_chunk_index = get_chunk_index(next_chunk_word_size);

  // 2. 优先走向全局复用池:试图从全局 ChunkManager 的 Free-List 中拦截并捞取被其他已死类加载器释放的 Chunk
  Metachunk* next_chunk = chunk_manager()->chunk_freelist_allocate(next_chunk_word_size, next_chunk_index);

  // 3. 缓存池未命中:说明全局没有死掉的类加载器留下的遗产,必须开辟新的虚拟疆土
  if (next_chunk == NULL) {
    // 调用全局单例虚拟空间列表,要求底层切出一个全新的物理连续的 Metachunk 块
    next_chunk = virtual_space_list()->get_new_chunk(next_chunk_word_size, next_chunk_index);
  }

  // 4. 安全防御:如果连底层系统也无法提供任何新块,说明内存彻底耗尽,向上传递失败信号
  if (next_chunk == NULL) {
    return NULL; 
  }

  // 5. 封存老 Chunk:将空间不足的老 _current_chunk 移出活跃区,挂载到当前加载器的 _chunks_in_use 历史归档链表中
  if (_current_chunk != NULL) {
    retire_current_chunk(); 
  }

  // 6. 激活新 Chunk:将新拿到的物理块接管为当前主活跃分配块,并更新持有的物理空间计数
  _current_chunk = next_chunk;
  _allocated_chunks_words += next_chunk_word_size;

  // 7. 在全新的、纯净的物理块内部执行指针碰撞,此次分配必然成功
  return _current_chunk->allocate(word_size);
}

3. 全局缓存层:ChunkManager::chunk_freelist_allocate

为了规避系统调用的开销,ChunkManager 通过多级自由链表数组(Free List)实现了解绑块的高效复用。

// hotspot/src/share/vm/memory/metaspace.cpp

Metachunk* ChunkManager::chunk_freelist_allocate(size_t word_size, ChunkIndex index) {
  // 全局锁保护:因为 ChunkManager 是整个 JVM 进程中所有类加载器共享的单例实体
  MutexLockerEx cl(SpaceManager::expand_lock(), Mutex::_no_safepoint_check_flag);

  // 1. 检查请求的 ChunkIndex 档位是否在合法链表范围内
  if (index >= NumberOfFreeLists) {
    return NULL; // HumongousChunk(特大块)通常有独立的字典管理,不走标准标准 FreeList
  }

  // 2. 获取对应尺寸级别的空闲链表(如 SmallChunkList)
  FreeBlockDictionary<Metachunk>* free_list = _free_chunks[index];
  if (free_list == NULL) {
    return NULL;
  }

  // 3. 从该空闲链表的头部摘除(Remove)一个现成的、之前被回收的 Metachunk
  Metachunk* chunk = free_list->get_chunk(word_size);
  if (chunk == NULL) {
    return NULL; // 当前档位的空闲链表为空,返回 NULL 让上层去切 OS 虚拟空间
  }

  // 4. 成功捞取物理块后,扣减全局空闲缓存的容量统计
  _free_chunks_total -= chunk->word_size();
  _free_chunks_count--;

  // 5. 将该 Chunk 的状态从 "Free" 隐式转换为 "In-use",并返回给请求的 SpaceManager
  return chunk;
}

4. 虚拟映射层:VirtualSpaceNode::get_chunk_vs 与内存 Commit

当全局复用池无法提供物理块时,最终必须依赖 VirtualSpaceNode 操作底层虚拟地址并驱动内核映射物理页。

// hotspot/src/share/vm/memory/metaspace.cpp

Metachunk* VirtualSpaceNode::get_chunk_vs(size_t chunk_word_size) {
  assert_lock_strong(SpaceManager::expand_lock()); // 确保在全局扩容锁的保护下运行

  size_t chunk_byte_size = chunk_word_size * BytesPerWord;

  // 1. 空间边界检查:利用当前 Node 内部的最高位游标(_top)加上需要的字节数,
  // 检查是否超出了当前 Node 预留(Reserved)的绝对边界地址(end())
  if (bottom() + chunk_byte_size > end()) {
    return NULL; // 当前虚拟空间节点已满,返回 NULL 触发上层去创建全新的 VirtualSpaceNode
  }

  // 2. 锁定当前高位未分配地址的起始点,作为新 Metachunk 的物理首地址
  char* chunk_limit = (char*)_top;
  
  // 3. 动态物理提交(Dynamic Commit):
  // 虽然底层可能通过 mmap 预留了数百兆的虚拟地址空间,但直到这一刻,JVM 才会调用 commit_impl 
  // 真正命令操作系统为这段即将使用的特定字节空间分配物理内存页(或映射到交换区)。
  // 这种渐进式提交是 Metaspace 内存弹性的根源。
  if (!commit_impl(chunk_limit, chunk_byte_size)) {
    return NULL; // 若 OS 物理内存耗尽,拒绝提交,抛出 NULL 间接引发 OOM
  }

  // 4. 物理页提交成功,将当前 Node 内部的最高位物理划分游标向前推进
  _top += chunk_word_size;
  
  // 递增当前 Node 内部持有的活跃 Chunk 计数器
  _container_count++;

  // 5. 使用 C++ Placement New 技术,在刚刚成功 Commit、物理可写的内存首地址上,
  // 构造、初始化 Metachunk 的 C++ 头对象,建立其与当前控制节点的反向指针绑定
  Metachunk* result = ::new (chunk_limit) Metachunk(chunk_word_size, this);
  
  return result;
}

5. 终端执行层:Metachunk::allocate 指针碰撞

这是整条分配链条的最末端。当一个纯净、空间充足的 Metachunk 被交付给最上层的分配请求时,它在内部执行纳秒级的快速切分。

// hotspot/src/share/vm/memory/metachunk.hpp

MetaWord* Metachunk::allocate(size_t word_size) {
  // 核心极致优化:没有任何复杂的 FreeList 检索,采用极致高效的指针碰撞(Bump-the-pointer)算法

  // 1. 检查当前块内部的空闲游标(_top)加上请求的空间,是否超过该块的物理硬边界(_end)
  if (_top + word_size <= _end) {
    // 2. 成功,记录当前的游标地址作为分配给 Java 元数据对象的物理首地址
    MetaWord* result = _top;
    
    // 3. 游标单向向高端地址推进指定的字长,为下一次并发/连续分配划分新的起跑线
    _top += word_size;
    
    // 4. 返回分配到的高位物理连续空间地址
    return result;
  }
  
  // 空间彻底耗尽,返回 NULL,将控制权交还给 SpaceManager 触发 grow_and_allocate 扩容
  return NULL;
}


三、 深度解析架metaspace的构设计精妙之处

通过分析以上 OpenJDK 8元空间分配的核心链路源码,可以总结出其空间管理在架构上的三个关键设计特质:

  1. 多层锁分离与粒度细化
    在真正分配具体的元数据对象(如方法、常量池)时,HotSpot 只需要获取类加载器级别(ClassLoaderData)的局部细粒度锁,在 Metachunk 内部进行极速的指针碰撞分配。只有当当前 Chunk 耗尽,需要向全局申请或开辟新 Chunk 时,才会去获取全局的 SpaceManager::expand_lock()。这种“局部高频无交集,全局低频强同步”的设计,完美释放了多线程类加载时的并发吞吐量。
  2. 零外部碎片化(Zero External Fragmentation)
    由于 Metachunk 内部的元数据对象大小不一(从几个字节到几百字节不等),如果在普通的堆空间中频繁分配和消亡,会产生极其可怕的物理内存碎片。元空间通过放弃单对象回收,改用“Chunk 内部纯指针碰撞、ClassLoader 卸载时全块整装回收”的策略,使 Chunk 内部的外部碎片率永久保持为零。
  3. 延迟提交与内存防线(Dynamic Commit)
    VirtualSpaceNode 通过将虚拟内存的“预留(Reserve)”与“提交(Commit)”在时间轴上完全解耦,实现了按需向操作系统索要物理页。结合 SpaceManager 的动态步长算法(根据加载历史智能改变申请 Chunk 的大小),确保了无论是微型反射类还是巨型核心类,元空间都能以最经济、最贴合的物理内存开销来支撑 JVM 的平稳运行。
Logo

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

更多推荐