directbytebuffer 的堆外内存回收依赖其堆内对象的 gc,cleaner 作为虚引用桥梁自动绑定并异步释放;可控的是堆内引用管理,而非 cleaner 执行时机。

直接内存的垃圾回收不是 JVM 主动“清理”,而是靠堆内 DirectByteBuffer 对象的生命周期被动触发——核心在于:堆外内存本身没有 GC,JVM 只管堆,而 Cleaner 是连接堆与堆外的自动桥梁。
Cleaner 不是手动开关,而是绑定即生效的虚引用封装
每次调用 ByteBuffer.allocateDirect(),JDK 内部会立刻执行三件事:
- 用
Unsafe.allocateMemory()向操作系统申请堆外内存 - 在 Java 堆中创建一个轻量级
DirectByteBuffer实例(只存地址、大小等元数据) - 同步调用
Cleaner.create(this, new Deallocator(...)),将该 buffer 和一个Cleaner绑定
这个绑定不可跳过、不可替换、不可延迟——Cleaner 是 final 类,构造器私有,你无法继承或传入自定义实现。
回收时机完全取决于堆中对象是否真正“不可达”
堆外内存能否释放,99% 看的是 DirectByteBuffer 对象能不能被 GC 掉。只要它还被任何强引用牵着,Cleaner 就永远不会执行。常见阻断点包括:
-
static字段或全局缓存(如ConcurrentHashMap)长期持有 buffer -
ThreadLocal存储未清理的 buffer,尤其在线程池复用场景下 - lambda 或匿名内部类隐式捕获了 buffer,延长其生命周期
-
slice()/duplicate()后的子 buffer,在 JDK 8 及更早版本中仍强引用父 buffer 的 cleaner - NIO Channel(如
FileChannel)未关闭,底层可能隐式复用并持有 buffer
Cleaner 的执行由 JVM 内部线程异步驱动,不走标准虚引用流程
虽然 Cleaner 继承自 PhantomReference,但它不走你提供的 ReferenceQueue:
- 它使用 JVM 内部静态的
dummyQueue,你调用queue.poll()或反射调用clean()都无效 - GC 完成后,JVM 的
ReferenceHandler线程扫描 cleaner 链表,发现可入队的就调用clean() -
clean()再触发Deallocator.run(),最终调用Unsafe.freeMemory()释放操作系统内存
真正可控的只有堆内引用管理,而非堆外释放逻辑
你无法控制 Cleaner 何时运行,但可以控制 DirectByteBuffer 何时变成垃圾:
- 避免长期持有:不用时及时置为
null,从集合或ThreadLocal中显式 remove - 慎用派生操作:对
slice()等返回的 buffer,注意其生命周期是否依赖父 buffer - 配合框架资源管理:如 Netty 的
PooledByteBufAllocator,通过buffer.release()主动归还池中内存 - 设硬上限兜底:
-XX:MaxDirectMemorySize可让Bits.reserveMemory()在超限时尝试触发 GC,但这只是试探,不是保障
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











