directbytebuffer 的 cleaner 由 jvm 的 referencehandler 线程异步驱动,不可手动调用或替换;其回收时机完全取决于 directbytebuffer 对象能否被及时 gc,而隐式强引用(如 static 持有、threadlocal、子 buffer 引用等)是堆外内存泄漏主因。

DirectByteBuffer 的 Cleaner 不是给你手动调用的,也不是靠 PhantomReference.poll() 主动触发的——它由 JVM 内部的 ReferenceHandler 线程异步驱动,你唯一能做的,是确保对象能被及时 GC,否则 Cleaner 永远不会 clean。
DirectByteBuffer 构造时 Cleaner 就已绑定,无法替换或延迟注册
每次 new DirectByteBuffer,JDK 都会自动执行 Cleaner.create(this, new Deallocator(...))。这个过程不可跳过、不可拦截、也不接受自定义 Cleaner 实例。
- 你不能传入自己的
PhantomReference子类,Cleaner是 final 类且构造器私有 -
Cleaner.create()内部会把新实例加入一个全局双向链表(first静态字段),防止 Cleaner 自身被提前回收 -
Deallocator里封装了base地址和size,clean 时调用unsafe.freeMemory(base),你无法修改这个行为
PhantomReference 在这里只是底层信号载体,不是你该直接操作的对象
虽然 Cleaner 继承自 PhantomReference,但它不走标准虚引用流程:它没用外部传入的 ReferenceQueue,而是用了内部 dummyQueue;它的入队、出队、clean 调用全部由 JVM 的 ReferenceHandler 线程在 GC 后统一处理。
- 别试图 new
PhantomReference去监控DirectByteBuffer——它已经自带 Cleaner,再套一层纯属冗余 - 别调用
queue.poll()或queue.remove()等待 Cleaner 入队——它根本不会进你创建的队列 -
Cleaner的clean()方法是 package-private,外部代码无法显式调用
真正影响回收时机的,是你对 DirectByteBuffer 引用的管理方式
堆外内存是否及时释放,99% 取决于 DirectByteBuffer 对象本身能否被 GC 掉。而它的存活周期常被隐蔽延长:
- 被 static 字段、缓存容器(如
ConcurrentHashMap)、线程局部变量(ThreadLocal)长期持有 - 在 lambda 或匿名内部类中意外捕获,导致隐式强引用
- 使用
asReadOnlyBuffer()、slice()、duplicate()后,子 buffer 仍强引用父 buffer 的cleaner(JDK 9+ 已修复部分场景,但低版本仍存在) - 未关闭 NIO Channel(如
FileChannel)时,底层可能复用并隐式持有所关联的DirectByteBuffer
手动触发回收只在极端调试场景下有效,且副作用明显
如果你真遇到堆外 OOM 且确认对象已无强引用,可临时辅助 GC,但生产环境禁止常规使用:
-
System.gc()可能触发 Cleaner 处理,但 JDK 8+ 默认禁用显式 GC(受-XX:+DisableExplicitGC影响),且强制 GC 开销巨大 -
Bits.reserveMemory()在分配前会尝试最多 9 次System.gc()+ sleep(100ms),这是 JDK 内置的兜底逻辑,不是你该模仿的模式 - 调用
sun.misc.Cleaner(或jdk.internal.ref.Cleaner)的clean()是反射黑科技,JDK 17+ 默认模块化限制,运行时报IllegalAccessException
最易被忽略的一点:堆外内存泄漏往往不是 Cleaner 失效,而是 DirectByteBuffer 对象根本没进 GC 队列——检查 heap dump 时重点看谁在引用它,而不是盯着 Cleaner 是否 clean。










