DirectByteBuffer 的 Cleaner 不可手动调用且不可靠,其 clean() 方法仅由 JVM 内部 ReferenceHandler 线程自动触发;手动调用无效且危险,System.gc() 触发的回收不可控,真正可控的是显式释放与资源约束。

DirectByteBuffer 的 Cleaner 不是手动回收接口
很多人误以为调用 Cleaner.clean() 就能立即释放堆外内存,这是危险的误解。Cleaner 本质是虚引用的封装,它的 clean() 方法只应在 JVM 内部由 ReferenceHandler 线程自动触发——你手动调用它,既不会释放内存(因为 Deallocator 的执行受内部状态保护),还可能破坏 Cleaner 链表结构,导致后续 GC 无法正常清理其他对象。
常见错误现象:
- 手动 new Cleaner(...) 并调用 clean() → 无效果,
unsafe.freeMemory()不被执行 - 反射获取
DirectByteBuffer.cleaner字段后强制调用clean()→ 可能抛NullPointerException或静默失败,且该对象仍保留在 Cleaner 链表中,形成“假释放”假象
System.gc() 触发的 Cleaner 回收不可靠
当 Bits.reserveMemory() 检测到堆外内存不足时,会主动调用 System.gc(),但这不是解决方案,而是兜底试探。它的实际效果取决于 JVM 参数和运行时状态:
- 若启用了
-XX:+DisableExplicitGC(生产环境常见),System.gc()直接退化为空操作,Cleaner 完全不会被处理 - 即使未禁用,Full GC 的时机也不可控:可能延迟数秒甚至数分钟,期间堆外内存持续泄漏
- 频繁调用
System.gc()会干扰 GC 正常节奏,引发 STW 时间突增,尤其在 G1 或 ZGC 下更明显
这不是“要不要调”的问题,而是“根本不能依赖”的问题。
Cleaner 机制本身存在异步与竞争风险
Cleaner 的整个生命周期由两个独立线程协作完成:ReferenceHandler(处理引用队列)和 Deallocator 执行线程(实际调用 unsafe.freeMemory())。这带来两个硬伤:
-
ReferenceHandler是低优先级守护线程,若系统负载高或队列积压,Cleaner 入队可能延迟,导致堆外内存“已无引用但未释放”窗口扩大 -
Deallocator.run()中的unsafe.freeMemory(address)是非原子操作;若此时发生 JVM crash 或address被意外复用(如 Unsafe 误操作),可能造成悬空指针或越界访问 - Cleaner 链表是单向链表 + volatile head,没有锁保护;并发大量创建/回收 DirectByteBuffer 时,链表断裂虽罕见,但一旦发生,对应堆外内存永久泄漏
真正可控的回收路径只有显式清理 + 资源约束
想让堆外内存行为可预测,必须绕过 Cleaner 的被动性,走确定性路径:
- 对 Netty、RocketMQ 等框架,启用其内置池化(如
PooledByteBufAllocator),并确保ByteBuf.release()被正确调用——这会归还 buffer 到池中,避免新分配 - 自研使用
ByteBuffer.allocateDirect()时,用try-with-resources包装自定义AutoCloseable实现,在close()中调用Bits.unreserveMemory()和unsafe.freeMemory()(需通过反射获取地址字段) - 强制限制总量:启动参数设死
-XX:MaxDirectMemorySize=512m,配合 NMT 监控jcmd,一旦接近阈值就熔断新分配VM.native_memory summary
Cleaner 是 JVM 给的“尽力而为”保障,不是安全绳。真正在意堆外内存生命周期的场景,得自己握紧释放的手柄,而不是等那个不确定的“通知”。










