cleaner 的 clean() 方法不可手动触发,它仅由 jvm 的 referencehandler 线程在 gc 后自动调用;手动反射调用既不保证释放,还可能破坏 cleaner 链表导致永久性堆外内存泄漏。

不能安全地“手动释放”——Cleaner 机制不是为手动调用设计的接口,强行调用 clean() 既不保证释放,还可能破坏 JVM 内部状态。
Cleaner 的 clean() 方法不可手动触发
Cleaner 是虚引用(PhantomReference)的封装,其 clean() 方法只应在 JVM 内部由 ReferenceHandler 线程自动调用。该调用受内部状态保护:
- DirectByteBuffer 构造时注册的 Cleaner 对象,其 clean() 执行依赖于 Deallocator 实例是否已准备好、且未被标记为“已清理”
- 手动反射调用 cleaner.clean():多数情况下静默失败;若 Cleaner 已被入队但尚未处理,可能抛 NullPointerException;即使成功返回,unsafe.freeMemory() 也未必执行
- 更危险的是:手动调用会绕过 Cleaner 链表维护逻辑,导致该 Cleaner 节点仍滞留在链表中,后续 GC 无法再识别并清理它,造成永久性堆外内存泄漏
System.gc() 不是可控的释放手段
有人试图用 System.gc() “催促” Cleaner 执行,但这在生产环境中极不可靠:
- 若启用 -XX:+DisableExplicitGC(绝大多数线上 JVM 默认开启),System.gc() 直接退化为空操作
- 即使未禁用,Full GC 触发时机完全不可控,延迟可达数秒至分钟级,期间堆外内存持续占用
- 频繁调用会干扰 G1/ZGC 的并发回收节奏,引发 STW 时间突增,甚至诱发内存抖动
真正可落地的显式释放方式
绕过 Cleaner 的被动性,走确定性路径:
- 优先使用池化方案:如 Netty 的 PooledByteBufAllocator,默认启用 noCleaner 模式,通过 Unsafe.freeMemory(address) 显式释放,且内存复用避免反复分配
- 自定义显式释放逻辑:若必须用 DirectByteBuffer,可在业务层封装 allocate + free 流程,用 Unsafe(需 --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED)直接调用 freeMemory(address),并确保 address 不被复用或泄露
- 严格资源约束:通过 -XX:MaxDirectMemorySize 限制上限,并配合监控(如 NMT 或 JMX 的 BufferPoolMBean)及时发现异常增长
为什么“反射调 Cleaner”在 JDK 9+ 更难生效
JDK 9 引入模块系统后,sun.misc.Cleaner 和 jdk.internal.ref.Cleaner 均被封装在非导出模块中:
- 反射获取 cleaner 字段或调用 clean() 方法,需额外 VM 参数(如 --add-opens=java.base/jdk.internal.ref=ALL-UNNAMED)才能绕过强封装
- 即便参数到位,clean() 方法本身在 jdk.internal.ref.Cleaner 中已被改为私有或委托给 Cleaner.Cleanable,原有反射路径极易失效
- Java 21 进一步收紧内部 API 访问,无授权反射将直接抛 InaccessibleObjectException
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











