DirectByteBuffer堆外内存由Cleaner与虚引用协作异步回收:构造时绑定Deallocator任务,GC判定对象不可达后,Reference Handler线程遍历Cleaner双向链表调用clean(),执行Unsafe.freeMemory释放内存。

DirectByteBuffer 的堆外内存不是靠 GC 主动回收的,而是靠 Cleaner 与虚引用协作,在对象被判定为垃圾后异步触发释放。
DirectByteBuffer 构造时就绑定了清理任务
每次调用 ByteBuffer.allocateDirect(),JVM 会执行以下关键步骤:
- 通过 Unsafe.allocateMemory() 向操作系统申请一大块物理内存
- 在堆内创建一个轻量级的 DirectByteBuffer 对象(仅几十字节)
- 立即调用 Cleaner.create(this, new Deallocator(base, size, cap))
- Cleaner 是 PhantomReference 的子类,它把当前 DirectByteBuffer 作为 referent,并把 Deallocator(一个 Runnable)存为清理逻辑
Cleaner 不依赖引用队列,而是靠双向链表主动管理
和普通虚引用不同,Cleaner 使用了一个静态双向链表(first 指针)自行维护所有活跃 Cleaner 实例:
- 构造时自动加入链表头部,防止 Cleaner 自身被提前回收
- 链表不依赖 ReferenceQueue —— 注释明确写明 dummyQueue “nothing will ever be placed on this queue”
- GC 完成后,JVM 内部的 Reference Handler 线程 会扫描这个链表,对已不可达的 referent(即 DirectByteBuffer)调用其 clean() 方法
clean() 调用 Deallocator.run() 完成真正释放
Cleaner 的 clean() 方法是整个释放流程的入口点:
- 先调用 remove(this) 将自己从链表中摘除,避免重复执行
- 再执行 this.thunk.run(),也就是 Deallocator 的 run() 方法
- Deallocator 内部调用 Unsafe.freeMemory(address) 归还物理内存,并同步调用 Bits.unreserveMemory() 更新 JVM 的直接内存计数器
回收时机不可控,但可被间接影响
这个过程是异步且延迟的,取决于 GC 频率和 Reference Handler 线程调度:
- 只要 DirectByteBuffer 还有强引用,哪怕只藏在一个 static Map 里,堆外内存就一直占用
- Full GC 更容易促使 Reference Handler 处理 Cleaner 链表,但不保证立即执行
- 手动调用 cleaner.clean() 可强制释放(需反射获取 cleaner 字段),但要注意线程安全和重复调用风险

















