bigywater.tech
Java 内部原理 · 86

Cleaner API 真实验证:替代 finalize() 的资源清理兜底机制

finalize() 早就被标记为废弃,官方推荐的替代方案是 java.lang.ref.Cleaner——这篇真实验证它的手动清理、幂等性,以及最关键的"忘记手动清理时是否真的能兜底"这件事。

clean()真实幂等
真实验证:手动调用close()后再重复调用两次,清理动作真实只执行了一次,不会重复触发
忘记close()真实也能兜底
真实验证:对象失去强引用后不手动清理,触发GC并等待,Cleaner后台线程真实自动执行了注册好的清理逻辑

上一篇:方法引用四种形式真实验证

真实验证:手动 close() 真实立刻触发清理

用Cleaner.register()注册一个不持有外部对象引用的清理动作,通过Cleanable.clean()手动触发。

CleanerApiDemo.java static final Cleaner cleaner = Cleaner.create(); class Resource implements AutoCloseable { Resource(int id, AtomicInteger counter) { this.state = new State(id, counter); // State不持有Resource本身的引用 this.cleanable = cleaner.register(this, state); } public void close() { cleanable.clean(); } }
真实实测
真实执行清理动作,资源id=1
手动close()后,真实清理计数: 1

真实结果证明手动调用 close()(内部真实转发到 Cleanable.clean())会立刻、同步地执行注册好的清理逻辑——这是 Cleaner 最推荐的使用方式:配合 try-with-resources 或显式 close() 做确定性的及时清理,Cleaner 提供的 GC 兜底只是"万一忘了"的保险。

真实验证:重复调用 close() 真实幂等

连续调用三次close(),验证清理逻辑是否会被重复执行。

真实实测
重复调用close()三次后,真实清理计数(应该还是1): 1

真实结果证明 Cleanable.clean() 内部真实做了状态标记,第一次调用真实执行清理逻辑并标记为"已清理",后续重复调用真实直接跳过,不会重复执行——这个幂等性保证在实际场景里很重要,因为资源类的 close() 经常会被多次调用(比如显式调用后又进入 try-with-resources 的自动关闭)。

真实验证:忘记手动清理,GC 后台真实自动兜底

最关键的一步:故意"忘记"调用close(),让对象失去所有强引用,验证GC发生后Cleaner是否真的会自动执行清理。

CleanerApiDemo.java static void createAndForget(AtomicInteger counter) { Resource leaked = new Resource(2, counter); // 不调用 close(),直接让 leaked 超出作用域 } // 之后真实多次 System.gc() 并等待
真实实测
真实执行清理动作,资源id=2
忘记调用close()、对象被GC后,Cleaner真实自动清理计数: 1(兜底机制,不保证立即执行,但真实会执行)
真实的兜底机制是怎么运作的。Cleaner.register() 内部真实用到了系列前面验证过的 弱引用/ReferenceQueue 机制:它不持有 Resource 对象本身的强引用,而是用一种特殊的引用类型追踪它。一旦 Resource 对象真实失去所有外部强引用、被判定可回收,这个追踪引用会被放入一个内部队列,Cleaner 启动的后台守护线程会真实从队列里取出它并执行注册时传入的清理逻辑(这里的 State.run())。真实验证也印证了官方文档反复强调的一点:这个机制不保证清理会"立刻"发生(取决于 GC 什么时候真的回收这个对象),所以两次真实运行都需要循环调用 System.gc() 并适当等待才能观察到效果——它是一个兜底保险,不能替代确定性的资源释放。清理逻辑本身(State 类)被设计成不能持有 Resource 实例的引用,这是为了避免清理动作本身又让即将被回收的对象重新可达,造成矛盾。

分步演示:Cleaner 的清理动作真实能在什么时候被触发

每一行都是真实运行这段代码得到的结果。

Cleaner真实支持完全不依赖GC的显式清理(和close()一样确定),同时保留GC触发的安全网机制——这是它相比finalize()真实的双重优势
这一步新验证的清理路径

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,手动清理和幂等性场景重复运行完全一致,GC兜底场景两次独立运行均真实触发了清理(等待多轮System.gc()后)。
    • 和finalize() 复活对象那篇一起看,能理解为什么 Cleaner 被设计出来取代 finalize()——finalize 允许对象在清理过程中"复活"自己,带来了各种诡异的语义问题,而 Cleaner 的清理逻辑被架构性地隔离,不可能引用到正在被清理的对象本身。
    • 没有涉及:PhantomReference 作为更底层的构建 Cleaner 类似机制的基础API、Cleaner在处理堆外内存(如DirectByteBuffer)释放场景下的真实应用——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号