cleaner 比 finalize 更可靠,因其基于虚引用和引用队列,由专用线程异步执行清理,不阻塞 gc;而 finalize 执行时机不可控、可能永不调用,且不保证调用次数。

为什么 Cleaner 比 finalize 更可靠
因为 finalize 的执行时机不可控、可能永不触发,且 JVM 不保证调用次数;而 Cleaner 基于虚引用(PhantomReference)+ 引用队列,由专门线程异步清理,不阻塞对象回收,也不影响 GC 效率。
常见错误现象:finalize 方法没被调用,或在应用退出前资源已泄漏;Cleaner 则只要对象被 GC,关联的清理动作大概率会被执行(除非 JVM 快速退出)。
- 使用场景:封装 native 资源(如文件句柄、内存映射区)、自定义池化对象、需要确定性释放但又不能依赖 try-with-resources 的情况
-
Cleaner实例是线程安全的,可复用;但每个待清理对象需注册独立的Cleanable - 性能影响极小——清理逻辑在单独后台线程运行,不参与 GC 周期
怎么注册一个 Cleaner 清理动作
不是直接 new Cleaner,而是通过 Cleaner.create() 获取单例,再调用其 register 方法绑定对象与清理逻辑。
示例:
立即学习“Java免费学习笔记(深入)”;
Cleaner cleaner = Cleaner.create();
MyResource resource = new MyResource();
Cleaner.Cleanable cleanable = cleaner.register(resource, () -> {
System.out.println("releasing native handle");
releaseNativeHandle(resource.handle);
});
注意:resource 必须是强引用对象;清理动作中不能持有该对象的强引用(否则阻止 GC),也不能抛出未捕获异常(会静默终止该 Cleaner 线程)。
- 参数差异:
register第一个参数是待监控对象,第二个是Runnable;JDK 17+ 支持带 cleanup key 的重载,用于取消注册 - 如果对象在注册前已不可达,
Cleanable会立即触发清理(但无法保证顺序) - 不要在
register后继续给resource赋新值——这会导致清理逻辑作用于错误实例
Cleaner 什么时候不工作
最常踩的坑是:JVM 进程提前退出,或者 Cleaner 后台线程被意外中断。
典型错误现象:System.exit(0) 或 Runtime.getRuntime().halt() 后,清理逻辑完全不执行;日志里看不到任何输出。
- 兼容性影响:JDK 9+ 才有
Cleaner,低版本必须降级到finalize或手动管理 - 如果清理逻辑里调用了阻塞 I/O 或锁竞争严重的方法,可能拖慢整个 Cleaner 线程,影响其他注册项
- 没有显式 shutdown 机制——JVM 正常退出时,Cleaner 线程会随 daemon 线程自然终止;但若需强制等待清理完成(如测试场景),得自己加同步手段
能否取消某个对象的清理注册
可以,但 JDK 16 之前不行。JDK 16 引入了 Cleanable.dispose(),调用后该清理动作不会再被执行。
示例:
立即学习“Java免费学习笔记(深入)”;
Cleaner cleaner = Cleaner.create(); Cleaner.Cleanable cleanable = cleaner.register(obj, cleanupAction); // …… cleanable.dispose(); // 之后 obj 被 GC 也不会触发 cleanupAction
注意:dispose() 是幂等的,但调用后再次 register 同一对象会产生新 Cleanable,旧的不会复活。
- 旧版 JDK(9–15)只能靠在清理逻辑里加标志位 + 外部状态判断,容易出竞态
- 不要在清理动作内部调用
dispose()——此时它已经失效,无意义 - 这个能力对资源复用场景很关键,比如对象池中对象被归还时主动取消清理











