元空间内存管理机制
- 前言
- 元空间内存管理机制
-
- [Metaspace 核心架构与设计哲学](#Metaspace 核心架构与设计哲学)
- 核心数据结构源码解析
-
- [1. 虚拟内存物理节点:VirtualSpaceNode](#1. 虚拟内存物理节点:VirtualSpaceNode)
- [2. 内存分配块:Metachunk](#2. 内存分配块:Metachunk)
- [3. 本地空间管理器:SpaceManager](#3. 本地空间管理器:SpaceManager)
- 核心内存分配算法源码深挖
-
- [1. 顶层入口:SpaceManager::allocate](#1. 顶层入口:SpaceManager::allocate)
- [2. 扩容分配核心:SpaceManager::grow_and_allocate](#2. 扩容分配核心:SpaceManager::grow_and_allocate)
- [3. Chunk 耗尽与分配失败处理 `SpaceManager::manage_allocation_failure`](#3. Chunk 耗尽与分配失败处理
SpaceManager::manage_allocation_failure) - [4. 虚拟空间节点切分 `VirtualSpaceNode::get_chunk_vs`](#4. 虚拟空间节点切分
VirtualSpaceNode::get_chunk_vs)
- 元空间的高效回收机制
-
- [1. 触发回收的时机](#1. 触发回收的时机)
- [2. 源码逻辑:Metaspace 的析构与冷冻](#2. 源码逻辑:Metaspace 的析构与冷冻)
- [3. 为什么看得到进程物理内存不下降?](#3. 为什么看得到进程物理内存不下降?)
- 总结:系统级调优启示
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
元空间内存管理机制
Metaspace 核心架构与设计哲学
在 OpenJDK 8u 中,永久代(PermGen)被完全移除,取而代之的是元空间(Metaspace)。这一改动最核心的动机是为了解决 PermGen 频繁触发 Full GC 以及 java.lang.OutOfMemoryError: PermGen space 的硬伤。PermGen 占据的是 JVM 堆的一部分连续内存,大小固定;而 Metaspace 则是直接利用本地内存(Native Memory)。
Metaspace 的底层管理采用了一种分层分配(Tiered Allocation)的架构。它并没有直接通过 malloc 为每一个类元数据分配内存,而是向操作系统申请大块内存,再自行进行细粒度的微调与分发。
核心组件拓扑关系
- ClassLoaderData (CLD) : 每个类加载器在 JVM 内部都对应一个
ClassLoaderData结构。类加载器的生命周期直接决定了其对应 Metaspace 内内存的生命周期。 - Metaspace : 属于
ClassLoaderData的成员,是面向外部的分配接口。它内部包含两个SpaceManager:一个用于普通元数据(Non-Class),另一个用于类指针元数据(Class,当启用了压缩类指针UseCompressedClassPointers时)。 - SpaceManager : 内存分配的实际操盘手。每个类加载器独占自己的
SpaceManager,负责向ChunkManager申请Metachunk,并在其内部实施指针碰撞(Pointer Bump)分配。 - ChunkManager : 全局单例(或按类型划分),作为
Metachunk的物理温床。它负责管理各种规格的空闲 Chunk,当SpaceManager撑满时,由它负责切分或分发新的 Chunk。 - VirtualSpaceList / VirtualSpaceNode : 整个 Metaspace 的最高物理抽象。
VirtualSpaceNode代表一块向操作系统mmap出来的连续虚拟内存空间(通常为 2MB 的倍数),VirtualSpaceList则将这些 Node 串联成单链表。
核心数据结构源码解析
以下源码均基于 OpenJDK 8u 标准实现。为了展现系统工程师视角下的内存管理,对关键逻辑进行了深度注释。
1. 虚拟内存物理节点:VirtualSpaceNode
文件路径:share/vm/memory/metaspace.cpp
VirtualSpaceNode 负责直接与 OS 交互,持有一整块通过 mmap 映射出来的匿名内存页。
cpp
class VirtualSpaceNode : public CHeapObj<mtClass> {
friend class VirtualSpaceList;
// 物理内存映射的底层封装,包含 reserved 空间和 committed 空间
VirtualSpace _virtual_space;
// 当前 Node 中已经被分配出去的 MetaWord 总数
size_t _top;
// 核心指针:指向当前 Node 内尚未被切分为 Chunk 的空闲内存起始地址
MetaWord* _bottom;
MetaWord* _end;
public:
// 从当前物理节点中切出一个 Metachunk
Metachunk* get_chunk_vs(size_t chunk_word_size);
// 初始化该节点,内部会调用 OS 层的内存预留接口
bool initialize();
};
// 具体的内存切分逻辑
Metachunk* VirtualSpaceNode::get_chunk_vs(size_t chunk_word_size) {
// 检查当前 Node 剩余空间是否足够切出目标大小的 Chunk
if (free_words_in_vs() < chunk_word_size) {
return NULL;
}
// 实施指针碰撞 (Pointer Bump) 核心分配
MetaWord* chunk_limit = top();
_top = _top + chunk_word_size; // 抬高顶级指针
// 将切出来的物理内存构建为一个 Metachunk 对象
Metachunk* result = ::new (chunk_limit) Metachunk(chunk_word_size, this);
return result;
}
2. 内存分配块:Metachunk
文件路径:share/vm/memory/metaspace.cpp
Metachunk 是分配给类加载器的基本单元。按照大小分为 SpecializedChunk (1KB/2KB), SmallChunk (4KB/8KB), MediumChunk (64KB/128KB) 和 HumongousChunk。
cpp
class Metachunk : public Metabase<Metachunk> {
friend class VMStructs;
// 所属的物理 VirtualSpaceNode
VirtualSpaceNode* const _container;
// 当前 Chunk 的顶部指针,随着类元数据的分配不断向前推进
MetaWord* _top;
MetaWord* const _bottom;
MetaWord* const _end;
// 标记当前 Chunk 是否已被放入全局空闲链表(即类加载器死掉后等待复用的 Chunk)
bool _is_tagged_free;
public:
// 在 Chunk 内部进行无锁的高效指针碰撞分配
MetaWord* allocate(size_t word_size) {
MetaWord* next = top();
// 空间边界检查
if (pointer_delta(end(), next) >= word_size) {
_top = next + word_size; // 指针步进
return next;
}
return NULL; // 当前 Chunk 空间耗尽
}
};
3. 本地空间管理器:SpaceManager
文件路径:share/vm/memory/metaspace.cpp
SpaceManager 追踪当前类加载器已经拥有的所有 Chunk。由于同一个类加载器加载类时可能发生并发(多线程调用 defineClass),因此对 SpaceManager 的操作需要加锁(MetaspaceAllocation_lock)。
cpp
// 源码文件:hotspot/src/share/vm/memory/metaspace.cpp
class SpaceManager : public CHeapObj<mtClass> {
friend class MetaspaceAux;
Metaspace::MetadataType _mdtype; // 元数据类型(Class类型或Non-Class类型)
Metaspace::MetaspaceType _space_type; // 空间类型(如 Standard, Boot, Anonymous 等)
Metachunk* _chunks_in_use[NumberOfInUseLists]; // 正在使用的不同大小 Chunk 的链表
Metachunk* _current_chunk; // 当前处于活跃分配状态的 Chunk
size_t _allocated_blocks_words; // 已分配的 Block 总大小(以字为单位)
size_t _allocated_chunks_words; // 已分配的 Chunk 总大小
// 锁对象,保护多线程并发分配
Mutex* const _lock;
public:
SpaceManager(Metaspace::MetadataType mdtype, Metaspace::MetaspaceType space_type, Mutex* lock);
// 分配入口
MetaWord* allocate(size_t word_size);
// 当当前 Chunk 耗尽时,扩容并获取新 Chunk
Metachunk* grow_chunks_by_size(size_t chunk_word_size, size_t block_word_size);
};
核心内存分配算法源码深挖
当 JVM 需要为新加载的类分配 InstanceKlass(或者 Method, ConstantPool)时,会调用 Metaspace::allocate。下面我们通过源码链路拆解其多层fallback的分配演算法。
1. 顶层入口:SpaceManager::allocate
文件路径:share/vm/memory/metaspace.cpp
cpp
MetaWord* SpaceManager::allocate(size_t word_size) {
MutexLockerEx cl(MetaspaceAllocation_lock, Mutex::_no_safepoint_check_flag);
size_t raw_word_size = get_raw_word_size(word_size);
MetaWord* result = NULL;
// 策略 1:首先尝试从当前持有的主力 Metachunk 中通过指针碰撞直接分配
if (_current_chunk != NULL) {
result = _current_chunk->allocate(raw_word_size);
}
// 策略 2:如果主力 Chunk 满了,尝试从之前打入冷宫的废弃小块(Free Blocks)中捡漏复用
if (result == NULL) {
result = block_freelist()->get_block(raw_word_size);
}
// 策略 3:如果全面宣告耗尽,只能执行慢速分配(向外扩容申请新的 Chunk)
if (result == NULL) {
result = grow_and_allocate(raw_word_size);
}
if (result != NULL) {
_allocated_blocks_words += raw_word_size;
}
return result;
}
2. 扩容分配核心:SpaceManager::grow_and_allocate
文件路径:share/vm/memory/metaspace.cpp
当旧 Chunk 无法容纳新对象时,SpaceManager 需要向全局的 ChunkManager 要一块新的 Metachunk。
cpp
MetaWord* SpaceManager::grow_and_allocate(size_t word_size) {
// 根据当前类加载器的类型(如:是 Bootstrap 还是普通的)和所需大小,计算出应该申请什么规格的 Chunk
ChunkIndex index = select_chunk_size(word_size);
size_t chunk_word_size = index_to_size(index);
// 向全局的 ChunkManager 申请一块干净的 Metachunk
Metachunk* next_chunk = chunk_manager()->chunk_freelist_allocate(chunk_word_size);
// 如果全局空闲链表里也没有现成的 Chunk
if (next_chunk == NULL) {
// 只能向最高层的 VirtualSpaceList 申请,在底层的操作系统虚拟内存节点 (VirtualSpaceNode) 上切一块出来
next_chunk = vsl->get_new_chunk(chunk_word_size, grow_chunks_by_words);
}
// 如果成功拿到了新 Chunk
if (next_chunk != NULL) {
// 把当前满了的旧 Chunk 扔进 in-use 链表中作归档记录
retire_current_chunk();
// 将新 Chunk 设为当前主力分配 Chunk
_current_chunk = next_chunk;
// 再次尝试在新 Chunk 中执行指针碰撞分配
return _current_chunk->allocate(word_size);
}
// 说明 OS 已经无法成功划拨 committed 内存,触发 OOM 隐患
return NULL;
}
3. Chunk 耗尽与分配失败处理 SpaceManager::manage_allocation_failure
当当前 Metachunk 满了,SpaceManager 会走以下逻辑:
cpp
// 源码文件:hotspot/src/share/vm/memory/metaspace.cpp
MetaWord* SpaceManager::manage_allocation_failure(size_t word_size) {
// 1. 确定需要申请的新 Chunk 的尺寸(Specialized, Small, Medium)
size_t next_chunk_size = calc_chunk_size(word_size);
// 2. 尝试从全局的 ChunkManager(空闲 Chunk 链表)中获取一个空闲的 Chunk
Metachunk* next_chunk = chunk_manager()->chunk_freelist_allocate(next_chunk_size);
// 3. 如果全局空闲链表中也没有合适的 Chunk,则需要向底层的 VirtualSpaceList 申请
if (next_chunk == NULL) {
next_chunk = VirtualSpaceList::get_new_chunk(next_chunk_size);
}
if (next_chunk != NULL) {
// 4. 将新申请到的 Chunk 挂载到当前 SpaceManager 的正在使用队列中
add_chunk(next_chunk, true);
// 5. 在全新的 Chunk 内部实施分配
return next_chunk->allocate(word_size);
}
return NULL; // 彻底宣告无法分配
}
4. 虚拟空间节点切分 VirtualSpaceNode::get_chunk_vs
最终,底层会调用 VirtualSpaceNode 对已 mmap 的本地内存执行指针碰撞(Bump-the-pointer)推进。
cpp
// 源码文件:hotspot/src/share/vm/memory/metaspace.cpp
Metachunk* VirtualSpaceNode::get_chunk_vs(size_t chunk_word_size) {
// 1. 检查当前 Node 的剩余空间是否足够切出该 chunk_word_size
if (free_words_in_vs() < chunk_word_size) {
return NULL;
}
// 2. 确保所需的虚拟内存已经过系统的 commit(分配物理页面)
if (!ensure_committed(chunk_word_size)) {
return NULL;
}
// 3. 指针碰撞(Bump the pointer)分配机制
Metachunk* result = (Metachunk*)_top;
_top += chunk_word_size; // 推进 top 指针
// 4. 增加节点内部的活跃容器计数
_container_count++;
// 5. 初始化这一块被切出来的 Metachunk
result->initialize(this, chunk_word_size);
return result;
}
元空间的高效回收机制
由于 Metaspace 的生命周期和 ClassLoader 绑定,这也意味着:Metaspace 无法对单个 Class 或 Method 进行局部内存回收。
1. 触发回收的时机
只有当整个类加载器(ClassLoader)被 GC 回收时,它对应的 Metaspace 空间才会被全量释放。
在 GC 的清理阶段(Clean-up Phase),JVM 会遍历整个 ClassLoaderDataGraph,找出那些已经死亡的类加载器。
2. 源码逻辑:Metaspace 的析构与冷冻
当类加载器死亡时,调用 ClassLoaderData::~ClassLoaderData(),进而触发 Metaspace 对象的析构:
当 ClassLoader 被宣告死亡时,GC 会调用 ClassLoaderDataGraph::do_unloading。
文件路径:hotspot/src/share/vm/classfile/classLoaderData.cpp
cpp
// 源码文件:hotspot/src/share/vm/classfile/classLoaderData.cpp
ClassLoaderData::~ClassLoaderData() {
// 销毁该类加载器持有的所有元空间
if (_metaspace != NULL) {
delete _metaspace;
}
}
文件路径:hotspot/share/vm/memory/metaspace.cpp
cpp
SpaceManager::~SpaceManager() {
// 必须持有分配锁,防止全局 ChunkManager 状态被破坏
MutexLockerEx fcl(MetaspaceAllocation_lock, Mutex::_no_safepoint_check_flag);
// 遍历所有当前类加载器曾经使用过的 Chunk 链表
for (ChunkIndex i = ZeroIndex; i < NumberOfInUseLists; i = next_chunk_index(i)) {
Metachunk* head = _chunks_in_use[i];
Metachunk* cur = head;
while (cur != NULL) {
Metachunk* next = cur->next();
// 注意:这里并没有直接将物理内存还给操作系统!
// 而是将这些 Chunk 重新归还给全局的 ChunkManager 空闲链表,实现内存的池化复用
chunk_manager()->return_chunks(i, cur);
cur = next;
}
}
}
3. 为什么看得到进程物理内存不下降?
从上述析构源码可以看出,当一个 ClassLoader 被回收后,其持有的 Metachunk 只是变为了"空闲标记",并被移入了全局的 ChunkManager 链表中,以供其他类加载器二次加载类时直接复用。
这种设计能够极大避免频繁向操作系统执行 brk 或 munmap 系统调用带来的上下文切换开销。只有当整个 VirtualSpaceNode 内部所有的 Metachunk 都变成空闲状态时,系统才会视情况解除这段内存页的 committed 状态,或者真正释放这块虚拟内存。
总结:系统级调优启示
理解了底层基于 Chunk 的管理逻辑后,我们可以得出以下三条非常具有实操价值的调优结论:
- Metaspace 也有碎片化隐患 :如果你的应用频繁创建大量的短生命周期类加载器(例如:反射动态代理、大量的动态元编程表达式脚本),这些类加载器会申请大量的
SpecializedChunk或SmallChunk。当它们死掉后,会产生大量零碎的空闲 Chunk。如果后续有巨大的HumongousChunk(比如极大的类方法)申请,这些小碎片将无法拼接复用,迫使 JVM 继续向系统申请新的VirtualSpaceNode,导致物理内存暴涨。 -XX:MetaspaceSize的本质 :这个参数不是"元空间的初始大小",而是触发 Metaspace 第一次垃圾回收的物理水位线(High Water Mark)。如果配置得太低,应用启动加载类时,只要一跃过该线,就会立刻引发一次 Full GC。因此,生产环境建议将其基准值设高(例如 256M 或 512M)。- 防止本地内存无限失控 :虽然元空间默认无上限(仅受限于物理内存),但必须配置
-XX:MaxMetaspaceSize。如果不加限制,一旦代码中出现类加载器泄漏(ClassLoader Leak),它将疯狂蚕食主机物理内存,最终导致触发 OS 的 OOM Killer 强行杀死整个 Java 进程。