揭秘JVM内存分类学机制
JVM内存分类学机制
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正
JVM内存分类学机制
在 OpenJDK 8的 HotSpot 虚拟机中,为了实现极致的性能和内存安全,开发人员并没有直接在业务代码中散落地使用 C++ 原生的 new 和 delete。相反,HotSpot 构建了一套极其严密的内存分配类层级结构(Allocation Class Hierarchy)。
所有的虚拟机内部对象(非 Java 对象,指虚拟机自身的 C++ 对象,如元数据、句柄、线程隔离分配区等)都必须继承自特定的基类。通过重载基类的 operator new 和 operator delete,HotSpot 强制约束了对象的分配位置与生命周期模式。其中最核心的三个内存基类定义在 src/share/vm/memory/allocation.hpp 中:
AllStatic:静态元基类。生命周期与进程全局或模块同生共死,绝不允许实例化。StackObj:栈分配基类。生命周期严格受限于 C++ 局部作用域(RAII),严格禁止堆分配。ResourceObj:资源区分配基类。支持多样化的生命周期,核心利用ResourceArea构筑弹性高效的 O ( 1 ) O(1) O(1) 批量擦除生命周期。
1. AllStatic:仅含静态成员的工具类(无实例生命周期)
源码精读与注释
在 OpenJDK 8中,AllStatic 的设计极其纯粹。它本身不包含任何成员变量,其生命周期属于编译期约束。它没有运行时的“对象生命周期”,因为其根本不允许在堆或栈上创建实例。
// 源码路径:hotspot/src/share/vm/memory/allocation.hpp
class AllStatic {
public:
// 重载类的 new 运算符并声明为不抛出异常(throw()),但内部没有具体实现,或者在子类中被绝对禁止。
// 通过将 new/delete 运算符暴露在 public 但不提供底层堆分配逻辑,
// 配合禁用分配的宏或纯静态规约,阻止其在堆上生成。
void* operator new(size_t size) throw();
void operator delete(void* p);
void* operator new[](size_t size) throw();
void operator delete[](void* p);
};
生命周期与核心机制深度解析
- 分配位置:无分配位置。
AllStatic的子类(例如Universe、Arguments、VM_Version)只包含静态成员变量(Static Fields)和静态函数。静态成员变量存储在 C++ 进程的全局/静态存储区(Data 段或 BSS 段)。 - 创建机制:在编译阶段,如果尝试对
AllStatic的子类执行new MyStaticClass()或在栈上声明MyStaticClass obj;,编译器会因为无法解析合法的分配行为或受制于非实例化规约而报错。 - 销毁机制:
- 静态成员变量:其生命周期等同于整个 JVM 进程的生命周期。它们在 JVM 进程启动、类库加载时初始化,在 JVM 进程退出(Exit)时由操作系统统一回收。
- 不触发析构函数:由于无法创建实例,子类中严禁定义非静态的构造与析构函数。所有初始化逻辑通常封装在静态的
initialize()函数中,销毁逻辑封装在destroy()或shutdown()中。
2. StackObj:绝对栈分配对象(受 C++ 栈帧作用域控制)
源码精读与注释
StackObj 用于强制约束对象只能分配在 C++ 的执行栈(Execution Stack)上。
// 源码路径:hotspot/src/share/vm/memory/allocation.hpp
class StackObj ALLOCATION_SUPER_CLASS_SPEC {
private:
// 【核心防御机制】将分配运算符声明在 private 访问权限下,且不提供具体实现。
// 这使得任何尝试执行 "new StackObj()" 的代码在编译时都会触发 "error: 'operator new' is a private member" 错误。
void* operator new(size_t size) throw();
void operator delete(void* p);
void* operator new[](size_t size) throw();
void operator delete[](void* p);
public:
// 默认构造函数
StackObj() {
// 在 DEBUG 模式下,HotSpot 可能会通过特定的栈边界检查机制(如 VerifyStack),
// 验证当前对象的 this 指针是否确实处于当前线程的栈内存范围内。
}
~StackObj() { }
};
生命周期与核心机制深度解析
-
分配位置:当前 C++ 线程的执行栈帧(Stack Frame)空间内。
-
创建机制(RAII 机制):
-
它的创建完全依赖于标准 C++ 的局部变量声明(例如:
ThreadStateTransition tst(thread);或ResourceMark rm;)。 -
当 C++ 线程执行流进入声明该对象的代码块(Scope
{ ... })时,编译器通过移动栈指针(Stack Pointer),在当前栈帧中为该对象分配空间,并调用构造函数。 -
销毁机制:
-
自动释放:当执行流离开该局部变量所属的作用域(遇到右大括号
}、return、break或抛出异常)时,C++ 运行时会自动、逆序地调用该作用域内所有StackObj的析构函数~StackObj()。 -
析构函数执行完毕后,随着当前整个 C++ 函数栈帧的弹出(Pop),该对象占用的栈内存被自动回收。
-
典型应用:各种句柄保护器、计数器、锁自动化管理类(如
MutexLocker、ResourceMark、HandleMark)。这类对象的生命周期必须严格与一段特定的代码执行逻辑绑定。
3. ResourceObj:多模态多维度资源对象(受 ResourceArea 与 ResourceMark 控制)
源码精读与注释
ResourceObj 是 HotSpot 中最复杂的内存管理基类。它支持多种分配模式,默认分配在资源区(ResourceArea)中。资源区是一种基于 Arena(区域块)的高效内存池,专门用于存放 JVM 运行过程中的临时中间数据(如 Compiler 编译期的 IR 节点、方法解析时的临时符号等)。
// 源码路径:hotspot/src/share/vm/memory/allocation.hpp
class ResourceObj ALLOCATION_SUPER_CLASS_SPEC {
public:
// 定义对象的分配类型标签,用于运行时识别该对象到底身处何方
enum allocation_type {
UNKNOWN_TYPE = 0, // 未知类型
RESOURCE_AREA, // 资源区分配(默认,依赖 ResourceArea)
C_HEAP, // C 堆分配(常规 malloc/free)
STACK // 栈分配
};
#ifdef ASSERT
private:
// 在 Debug 模式下,用于追踪和验证分配类型的辅助字段
unsigned uintptr_t _allocation_t;
public:
// 将分配类型和当前对象的地址进行编码存储,用于后续安全检查
void set_allocation_type(address res, allocation_type type);
allocation_type get_allocation_type() const;
bool is_resource_obj() const { return get_allocation_type() == RESOURCE_AREA; }
bool is_C_heap_obj() const { return get_allocation_type() == C_HEAP; }
bool is_stack_obj() const { return get_allocation_type() == STACK; }
#endif // ASSERT
public:
// 默认构造函数:处理非 new 方式直接声明(如栈声明)的类型标记
ResourceObj();
// 拷贝构造函数
ResourceObj(const ResourceObj& r);
// 赋值运算符
ResourceObj& operator=(const ResourceObj& r);
// 析构函数
~ResourceObj();
// 【核心重载 1】默认的 new 运算符 -> 投递到 RESOURCE_AREA
// 示例:MyResourceObj* p = new MyResourceObj();
void* operator new(size_t size, JVMFlag flag) throw();
// 【核心重载 2】指定分配类型的 new 运算符
// 示例:MyResourceObj* p = new(ResourceObj::C_HEAP, mtInternal) MyResourceObj();
void* operator new(size_t size, allocation_type type, MEMFLAGS flags) throw();
// 重载对应的数组分配运算符
void* operator new[](size_t size, JVMFlag flag) throw();
void* operator new[](size_t size, allocation_type type, MEMFLAGS flags) throw();
// 重载 delete 运算符
void operator delete(void* p);
void operator delete[](void* p);
};
深入追溯源码:ResourceObj 如何感知自身的生命周期?
由于 C++ 的 operator new 只能返回 void* 内存指针,而在执行子类的构造函数时,子类并不知道自己是通过 RESOURCE_AREA 还是 C_HEAP 分配出来的。HotSpot 在这里使用了一个非常巧妙的方法(通过线程局部变量传递状态)。
我们看 allocation.cpp 中对应的 operator new 实现:
// 源码路径:hotspot/src/share/vm/memory/allocation.cpp
void* ResourceObj::operator new(size_t size, allocation_type type, MEMFLAGS flags) throw() {
void* p;
switch (type) {
case RESOURCE_AREA:
// 从当前线程的 ResourceArea 中切分一块内存(极其高效,只需移动指针)
p = Thread::current()->resource_area()->allocate_bytes(size);
break;
case C_HEAP:
// 调用底层 os::malloc 分配标准的 C 堆内存,并记录 Native 内存追踪(NMT)
p = os::malloc(size, flags);
break;
case STACK:
// 显式声明为 STACK 的特殊处理
p = os::malloc(size, flags);
break;
default:
fatal("Unknown allocation type");
}
#ifdef ASSERT
// 【核心黑科技】在多线程并发或单线程嵌套分配时,为了让接下来的构造函数能感知到分配类型,
// HotSpot 会把当前分配出的指针 'p' 和类型 'type' 临时挂载到当前线程的 ThreadLocalStorage 中。
// 紧接着调用构造函数时,构造函数内部会从线程中拉取这个状态,填充到对象的 _allocation_t 字段中。
Thread::current()->set_resource_obj_state(p, type);
#endif
return p;
}
ResourceObj::ResourceObj() {
#ifdef ASSERT
// 构造函数执行:从当前线程捞出刚刚在 operator new 中存入的类型信息
_allocation_t = Thread::current()->clear_resource_obj_state(this);
#endif
}
生命周期与核心机制深度解析
ResourceObj 的生命周期高度取决于它被实例化时的 allocation_type:
A. 当 type == RESOURCE_AREA(默认情况)
- 创建:通过
new MyResourceObj()。内存直接从当前线程关联的ResourceArea里的当前大内存块(Chunk)中切分出来。不调用操作系统的malloc,性能极高。 - 销毁(批处理机制):
- **不触发显式
delete**:绝对不能对RESOURCE_AREA类型的对象调用delete p;。因为它的operator delete对此类内存通常是空操作,或者仅在 Debug 模式下做校验。 - 受
ResourceMark管辖:它的生命周期完全由当前作用域下的ResourceMark决定。 - 当代码执行流进入某个方法时,通常会声明一个
ResourceMark rm;(这是一个StackObj)。ResourceMark会记录当前ResourceArea的内存顶部指针(Top Top-of-Chunk)。 - 随后在这个作用域内分配的所有
ResourceObj都排列在ResourceArea中。 - 当离开作用域,
ResourceMark析构时,它会直接将ResourceArea的顶部指针回滚(Rollback)到进入作用域时的位置。 - 致命隐患:这意味着在
ResourceArea释放时,**不会调用这些ResourceObj的 C++ 析构函数~ResourceObj()**!如果你的子类在析构函数里写了释放额外原生系统资源(如关闭文件描述符)的逻辑,这些逻辑将永远不会执行。因此,RESOURCE_AREA对象内部持有的成员也必须是能被批量安全抹除的纯内存对象。
B. 当 type == C_HEAP
- 创建:通过
new(ResourceObj::C_HEAP, mtInternal) MyResourceObj()。从操作系统的堆空间(Native Heap)分配内存。 - 销毁:必须显式调用
delete obj;。此时会触发重载的operator delete,内部最终调用os::free(obj)。在调用os::free之前,C++ 运行时会**正常调用其析构函数~ResourceObj()**。
C. 当 type == STACK
- 创建:直接在 C++ 栈上声明:
MyResourceObj obj;。 - 销毁:遵循常规
StackObj机制。在当前局部代码块退出时,自动触发析构函数并释放栈帧。
4. 终极对比总结表
| 核心维度 | AllStatic |
StackObj |
ResourceObj (默认模式) |
ResourceObj (C_HEAP 模式) |
|---|---|---|---|---|
| 内存分配位置 | 不分配实例内存(仅全局静态区) | C++ 执行栈(Stack Frame) | 线程专属资源区(ResourceArea) |
操作系统的原生堆(Native Heap) |
| 分配开销 | 无 | 极低(仅移动栈指针 SP) |
极低(仅移动 ResourceArea 的指针) |
较高(涉及系统调用/全局堆锁 malloc) |
| 生命周期控制者 | 进程(随 JVM 启动与退出) | 编译期词法作用域(RAII) | 显式声明的 ResourceMark 边界 |
开发者手动控制(必须匹配 delete) |
| 析构函数是否执行 | 否(根本无实例) | 是(离开作用域时自动执行) | 否(被 ResourceMark 批量重置指针,不调析构) |
是(执行 delete 时被调用) |
| 典型应用场景 | 全局工具类(如 Universe) |
临时保护器/锁管理(如 MutexLocker) |
编译期临时节点、符号解析中间件 | 寿命较长且大小不可控的运行时元数据 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)