前言

本文旨在记录近期研读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.hppreal_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;
}

追踪链路:

  1. GC 前Handle h -> Slot P -> Object A
  2. GC 移动:对象从 A 搬到 B
  3. GC 更新:GC 找到 Slot P,发现它存的是 A,将其原地改为 B
  4. 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.cppinitialize(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(资源获取即初始化)机制:

  1. 构造时:记录当前线程 HandleArea 的 Top 指针位置。
  2. 析构时:将 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++ 代码

通过这套基于 HandleAreaHandleMark 的二级指针机制,OpenJDK 成功在 C++ 的静态世界与 Java 堆的动态世界之间划定了一道安全的缓冲区。

更多推荐