DirectByteBuffer堆外内存回收依赖JVM自动联动机制:对象被GC回收后,Cleaner(虚引用封装)触发Deallocator执行unsafe.freeMemory。关键在及时释放强引用,避免因static缓存、ThreadLocal、NIO Channel未关闭等导致对象长期存活。

DirectByteBuffer 的堆外内存回收,靠的不是你手动 new PhantomReference 或调用 Cleaner.clean(),而是 JVM 内部一套自动联动机制。关键不在“怎么注册”,而在“怎么让对象及时被 GC”。
Cleaner 本质是 PhantomReference 的定制封装,但你不该直接操作它
- Cleaner 是 final 类,构造器私有,无法继承或替换
- 每次 new DirectByteBuffer,JDK 自动调用
Cleaner.create(this, new Deallocator(...)),绑定不可撤销 - 它内部用的是 dummyQueue(空队列),不走你创建的 ReferenceQueue;你调用
queue.poll()或remove()完全无效 -
clean()方法是 package-private,外部代码无法显式触发
真正决定堆外内存是否释放的,是 DirectByteBuffer 对象本身的可达性
- 只要这个对象还被强引用着,哪怕只有一处隐式持有,Cleaner 就永远不会执行
- 常见“隐形延长生命周期”的情况包括:
- 被 static 缓存、ConcurrentHashMap、Guava Cache 等长期持有
- 存在 ThreadLocal 引用(尤其在线程池中复用线程时)
- Lambda 表达式或匿名内部类意外捕获了 buffer 实例
- 调用
slice()、duplicate()、asReadOnlyBuffer()后,子 buffer 在 JDK 8 及更早版本中仍强引用父 buffer 的 cleaner - NIO Channel(如 FileChannel、SocketChannel)未 close,底层可能复用并隐式持有 buffer
如果你要自己管理非 ByteBuffer 的堆外资源(比如自定义 native buffer、GPU 显存等)
- 才需要手动套 PhantomReference + ReferenceQueue 模式
- 步骤是:分配资源 → 创建 PhantomReference 关联 ReferenceQueue → 启一个守护线程循环
queue.remove()→ 在回调中调用free()或close() - Java 9+ 推荐用
jdk.internal.ref.CleanerAPI,它封装了虚引用样板逻辑,更安全、不易出错 - 切记:清理逻辑必须轻量,不能含网络调用、磁盘 IO 或同步锁,否则会卡住 JVM 的 ReferenceHandler 线程
别迷信 System.gc() 或 -XX:+DisableExplicitGC
-
Bits.reserveMemory()在分配前可能触发System.gc(),但这是 JDK 内部行为,不可控 - 显式调用
System.gc()效果差、开销大,且在-XX:+DisableExplicitGC下完全失效 - 堆外 OOM 往往不是因为没 clean,而是因为 buffer 对象一直活在堆里 —— 解决引用泄漏比调优 GC 更有效
不复杂但容易忽略

















