揭秘Java世界中oop-klass模型奥秘之handle底层代码安全的保护伞
handle底层代码安全的保护伞
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
handle底层代码安全的保护伞
在 OpenJDK 8的 HotSpot 虚拟机中,Handle(句柄) 是连接 C++ 虚拟机内核代码与动态变化的 Java 堆对象之间的核心桥梁。作为程序员,理解 Handle 的核心在于理解:如何在不停止世界(STW)时间过长的前提下,让 C++ 代码安全地引用那些随时可能被 GC 移动的对象。
1. 核心矛盾:对象移动与指针失效
在 HotSpot 中,大多数垃圾回收器(如 ParNew、Parallel GC、G1)都会进行对象移动(Evacuation 或 Compaction)。
- Raw oop (原始指针):直接指向 Java 堆中对象的起始地址。
- 风险:如果 C++ 内部变量直接持有
oop,一旦触发 GC 导致对象从地址 A A A 搬迁到地址 B B B,该 C++ 变量将变成“野指针”,导致 VM 崩溃。
二级指针(Double Indirection) 正是解决这一问题的经典方案。
2. Handle 的定义与结构
在源码 hotspot/src/share/vm/runtime/handles.hpp 中,Handle 被定义为一个轻量级的包装类,其核心成员是一个二级指针 oop*。
源码剖析:Handle 类
class Handle VALUE_OBJ_CLASS_SPEC {
private:
oop* _handle; // 二级指针:指向存储 oop 地址的内存空间
public:
// 默认构造函数
Handle() : _handle(NULL) {}
// 核心构造函数:从 oop 转换为 Handle
Handle(Thread* thread, oop obj);
// 重载操作符,让 Handle 用起来像原始指针
oop operator -> () const { return obj(); }
oop obj() const {
// 解引用:通过二级指针获取当前对象的真实物理地址
return (_handle == NULL) ? (oop)NULL : *_handle;
}
// ...
};
3. HandleArea:存放二级指针的“稳定岛”
Handle 内部的 _handle 指针并不是指向堆,而是指向一个专门的内存区域:HandleArea。
每个线程(Thread 对象)都持有一个私有的 HandleArea。当你在 C++ 代码中创建一个 Handle 时,VM 会在当前线程的 HandleArea 中分配一个槽位(Slot),把对象的当前地址存进去,并返回这个槽位的地址。
源码剖析:分配逻辑 (handles.cpp)
hotspot\src\share\vm\runtime\handles.cpp中分配逻辑涉及的代码如下:
Handle::Handle(Thread* thread, oop obj) {
if (obj == NULL) {
_handle = NULL;
} else {
// 在线程私有的 HandleArea 中申请一个 oop 大小的空间
_handle = thread->handle_area()->allocate_handle(obj);
}
}
// allocate_handle 实际上是简单的指针偏移分配
oop* HandleArea::allocate_handle(oop obj) {
// 获取下一个可用的槽位地址
return real_allocate_handle(obj);
}
hotspot\src\share\vm\runtime\handles.hpp中real_allocate_handle源码如下:
oop* real_allocate_handle(oop obj) {
oop* handle = (oop*) (UseMallocOnly ? internal_malloc_4(oopSize) : Amalloc_4(oopSize));
oop* handle = (oop*) Amalloc_4(oopSize);
// 将对象的物理地址(oop)存入槽位
*handle = obj;
return handle;
}
4. 追踪与更新:GC 如何处理二级指针
这是 Handle 机制最精妙的地方。当 GC 发生并移动对象时,它不需要搜索整个 C++ 栈帧去寻找哪些地方用了原始指针,它只需要扫描 HandleArea。
4.1 根扫描(Root Scanning)
JVM 将所有活跃线程的 HandleArea 视为 GC Roots 的一部分。在 GC 的根枚举阶段,HandleArea 存储的所有 oop 都会被标记。
4.2 自动更新
在源码 hotspot/src/share/vm/runtime/handles.cpp 中,通过 oops_do 方法,GC 迭代器会遍历所有 Handle。
void HandleArea::oops_do(OopClosure* f) {
uintx handles_visited = 0;
// First handle the current chunk. It is filled to the high water mark.
// 遍历 Chunk 中所有的 oop* 槽位
handles_visited += chunk_oops_do(f, _chunk, _hwm);
// Then handle all previous chunks. They are completely filled.
Chunk* k = _first;
while(k != _chunk) {
handles_visited += chunk_oops_do(f, k, k->top());
k = k->next();
}
// 省略部分代码
if (_prev != NULL) _prev->oops_do(f);
}
static uintx chunk_oops_do(OopClosure* f, Chunk* chunk, char* chunk_top) {
oop* bottom = (oop*) chunk->bottom();
oop* top = (oop*) chunk_top;
uintx handles_visited = top - bottom;
// during GC phase 3, a handle may be a forward pointer that
// is not yet valid, so loosen the assertion
// f 是一个 OopClosure,如果是移动式 GC,f 会执行更新逻辑
// 如果 *p 指向的对象移动了,f->do_oop(p) 会把 *p 的值修改为对象的新地址
while (bottom < top) {
f->do_oop(bottom++);
}
return handles_visited;
}
追踪链路:
- GC 前:
Handle h->Slot P->Object A。 - GC 移动:对象从
A搬到B。 - GC 更新:GC 找到
Slot P,发现它存的是A,将其原地改为B。 - C++ 访问:
h->obj()依然访问Slot P,但此时读到的是B。二级指针的地址P始终未变,保证了 C++ 代码的稳定性。
5. 生命周期管理:HandleMark
为了防止 HandleArea 随时间无限增长,JVM 使用了 HandleMark 来管理 Handle 的生命周期。这是一种典型的 RAII(资源获取即初始化)模式。
源码剖析:HandleMark
class HandleMark {
private:
Thread* _thread;
HandleArea* _area;
Chunk* _chunk; // 记录进入时的 Chunk
// 记录进入时的栈顶位置
char *_hwm, *_max; // saved arena info
size_t _size_in_bytes; // size of handle area
HandleMark* _previous_handle_mark;
public:
HandleMark(); // see handles_inline.hpp
HandleMark(Thread* thread) { initialize(thread); }
~HandleMark();
hotspot\src\share\vm\runtime\handles.cpp中initialize(Thread* thread)和~HandleMark()源代码如下:
void HandleMark::initialize(Thread* thread) {
_thread = thread;
// Save area
_area = thread->handle_area();
// Save current top
// 压入当前线程的 HandleMark 链表
_chunk = _area->_chunk;
_hwm = _area->_hwm;
_max = _area->_max;
_size_in_bytes = _area->_size_in_bytes;
// Link this in the thread
set_previous_handle_mark(thread->last_handle_mark());
thread->set_last_handle_mark(this);
}
HandleMark::~HandleMark() {
HandleArea* area = _area; // help compilers with poor alias analysis
// Delete later chunks
if( _chunk->next() ) {
// reset arena size before delete chunks. Otherwise, the total
// arena size could exceed total chunk size
area->set_size_in_bytes(size_in_bytes());
_chunk->next_chop();
}
// Roll back arena to saved top markers
// 析构时恢复 HandleArea 的指针,相当于“回滚”了在当前作用域创建的所有 Handle
area->_chunk = _chunk;
area->_hwm = _hwm;
area->_max = _max;
Atomic::dec(&_nof_handlemarks);
// Unlink this from the thread
_thread->set_last_handle_mark(previous_handle_mark());
}
HandleMark:生命周期与自动清理
由于 HandleArea 随线程一直存在,如果不及时清理,会导致内存溢出。JVM 引入了 HandleMark 来管理 Handle 的作用域。
源码实现原理
HandleMark 利用了 C++ 的 RAII(资源获取即初始化)机制:
- 构造时:记录当前线程
HandleArea的 Top 指针位置。 - 析构时:将
HandleArea的 Top 指针恢复到记录的位置,从而“批量释放”在作用域内创建的所有 Handle 占用的 slot。
void Example() {
Thread* thread = Thread::current();
HandleMark hm(thread); // 记录当前位置
Handle h1(thread, obj1);
Handle h2(thread, obj2);
// 执行业务逻辑...
} // hm 析构,h1 和 h2 在 HandleArea 中占用的空间被回收
工程实践建议:
在编写 JVM 内部功能(如新的字节码指令、JNI 增强等)时,如果你调用的函数可能会触发 GC(例如分配内存、类加载、调用 Java 方法),你必须使用 Handle 而非 oop,并且在适当的作用域声明 HandleMark,否则会导致不可预知的内存损坏。
6. 核心架构总结
| 组件 | 内存位置 | 性质 | 作用 |
|---|---|---|---|
| oop | Java Heap | 原始指针 | 指向具体的 Java 对象实例,地址不稳定。 |
| HandleArea Slot | Native Memory (Thread Local) | 中间层地址 | 存放 oop 的真实地址。它是 GC 更新引用的唯一窗口。 |
| Handle | C++ Stack | 逻辑封装 | 存储二级指针,提供给 C++ 开发者的安全操作接口。 |
7. 总结对比
| 特性 | Raw oop (原始指针) | Handle (二级指针) |
|---|---|---|
| 指向目标 | 直接指向 Java 堆对象 | 指向 HandleArea 中的槽位 |
| 稳定性 | 极差(GC 后失效) | 极强(GC 自动更新内部值) |
| 访问开销 | 低(直接寻址) | 略高(需多一层内存间接寻址) |
| 扫描成本 | 高(需扫描 C++ 栈帧查找指针) | 低(仅需扫描连续的 HandleArea) |
| 主要用途 | 寄存器级优化、JIT 编译后的代码 | JVM 解释器、运行时 C++ 代码 |
通过这套基于 HandleArea 和 HandleMark 的二级指针机制,OpenJDK 成功在 C++ 的静态世界与 Java 堆的动态世界之间划定了一道安全的缓冲区。
更多推荐


所有评论(0)