Java Cleaner 怎样注册可触发的兜底清理动作
如果一个 Java 对象包着文件描述符、临时目录或本地句柄,单纯等待垃圾回收来释放资源,时机就不可控;但把清理动作写成普通匿名类,又可能反过来持有对象本身,导致 Cleaner 永远等不到它变成 phantom reachable。比较稳妥的做法是:用独立的静态清理状态注册动作,保存 Cleanable,正常路径显式调用 clean(),Cleaner 只负责最后兜底。
官方地址:https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/ref/Cleaner.html
一句话记忆:Cleaner 解决“对象意外忘记释放后的补救”,不替代确定性的close()。清理动作必须能独立完成,不能捕获被注册对象;正常关闭时仍然应由业务代码显式调用Cleanable.clean()。
我会把问题拆成三层:先确认 Cleaner 的作用域,再设计不反向持有业务对象的状态类,最后按资源的时效性决定是否使用它。
先划清 Cleaner 的职责边界
Cleaner 管理对象引用和对应的清理动作。当被注册对象变成 phantom reachable 后,Cleaner 线程才会尝试执行动作;动作最多执行一次,提前调用 Cleanable.clean() 后,后续不会再次清理。这个机制适合做“忘记关闭”的最后保险,不适合拿来承诺某个时间点一定完成释放。
官方 API 还特别提醒:清理动作不能持有被注册对象的引用。否则对象仍被清理动作间接强引用,无法进入 Cleaner 等待的可达性状态。清理动作抛出的异常会被忽略,所以动作本身应短小、可重复设计,并把必要的错误记录放在独立的日志或计数设施中。
把清理状态放进独立静态对象
下面的封装让 Resource 只负责持有句柄和 Cleanable,真正的关闭逻辑放进静态嵌套的 CleanupState。静态类不会偷偷捕获外层对象,清理动作只接触它自己保存的句柄。
import java.lang.ref.Cleaner;
public final class Resource implements AutoCloseable {
// 一个库通常可以共享 Cleaner,避免每个对象创建独立线程。
private static final Cleaner CLEANER = Cleaner.create();
// 静态状态只保存释放句柄,不保存 Resource 引用。
private static final class CleanupState implements Runnable {
private long nativeHandle;
private CleanupState(long nativeHandle) {
this.nativeHandle = nativeHandle;
}
@Override
public void run() {
// 先交换句柄,保证显式 clean 与兜底 clean 都只释放一次。
long handle = nativeHandle;
nativeHandle = 0L;
if (handle != 0L) {
// 这里替换成真实的本地资源释放调用。
releaseHandle(handle);
}
}
}
private final CleanupState state;
private final Cleaner.Cleanable cleanable;
public Resource(long nativeHandle) {
// 注册对象与动作,并保存 Cleanable 供 close 使用。
this.state = new CleanupState(nativeHandle);
this.cleanable = CLEANER.register(this, state);
}
@Override
public void close() {
// 正常路径立即清理,Cleaner 只作为遗漏关闭时的兜底。
cleanable.clean();
}
private static void releaseHandle(long handle) {
// 这里写真实释放逻辑,示例只保留资源句柄边界。
System.out.println("release handle: " + handle);
}
}

这里有两个容易漏掉的细节。第一,Cleanable 必须保存为字段,否则对象没有一个清晰的显式清理入口。第二,清理动作里的句柄要在释放前交换为零,避免 close() 和 GC 兜底同时到达时重复释放同一资源。
保存 Cleanable 并支持显式清理
把 Resource 声明为 AutoCloseable 后,调用方可以把它放进 try-with-resources。此时资源释放不依赖 GC,cleanable.clean() 的重复调用也不会再次执行真正的句柄释放。
public final class ResourceUsage {
public static void main(String[] args) {
try (Resource resource = new Resource(42L)) {
// 业务代码只使用资源,不需要接触 Cleaner 细节。
use(resource);
}
// 离开 try 后调用 close,正常释放路径已经完成。
}
private static void use(Resource resource) {
// 这里放实际业务操作;示例不运行外部资源。
System.out.println("use resource");
}
}
如果调用方没有关闭对象,Cleaner 仍可能在对象进入 phantom reachable 后执行状态类中的动作,但官方不保证 System.exit 时一定完成清理。也就是说,Cleaner 不能用来替代事务提交、响应发送、锁释放或任何要求调用方立刻看到结果的动作。
按风险决定是否采用 Cleaner
我在选型时会先看资源的释放要求,而不是先问“能不能用 Cleaner”。文件句柄、临时目录和本地内存通常应有明确的 close() 或释放 API;Cleaner 可以补上遗漏关闭的后手。若清理动作会等待网络、获取业务锁或执行长时间计算,就不应把它放进共享 Cleaner 的线程里,因为一个阻塞动作可能拖慢同一 Cleaner 上的其他清理动作。

| 场景 | 优先方案 | Cleaner 的位置 |
|---|---|---|
| 必须立刻释放的外部资源 | try-with-resources 或显式 close | 只做遗漏关闭的兜底 |
| 释放动作很短且只依赖独立句柄 | close 加 Cleaner | 适合作为第二道保险 |
| 清理需要阻塞或依赖业务对象 | 显式生命周期管理 | 不宜直接交给 Cleaner |
最后再检查三个失效点:清理动作是否捕获了 this;Cleanable 是否一直保存;清理逻辑是否能在重复触发和异常被忽略的情况下保持安全。只要其中一项答不上来,就先回到显式资源管理,而不是继续增加 Cleaner 注册。
常见问题
Cleaner 会在对象离开作用域后马上执行吗?
不会。作用域结束只代表局部变量不再使用,Cleaner 还要等对象达到相应的可达性状态;因此它不能作为精确的释放时钟。
为什么清理动作不能写成普通内部类?
普通内部类通常隐含持有外层实例,清理动作就可能反向保留被注册对象。把状态类做成静态嵌套类,并只保存独立句柄,可以避开这个引用链。
调用 clean 两次会释放两次吗?
Cleanable.clean() 的动作最多执行一次;示例中的句柄置零又增加了一层防御,便于让资源释放逻辑具备幂等边界。
总结
Java Cleaner 的正确用法不是“把 close 交给 GC”,而是注册一个不持有业务对象的独立清理状态,并把 Cleanable 保存下来。正常路径调用 close() 和 clean(),意外遗漏时再让 Cleaner 兜底;遇到阻塞、强时效或依赖对象状态的清理任务,仍应坚持显式生命周期管理。
Java Files.walk 返回的 Stream 怎样正确关闭
- 上一篇
- Java Files.walk 返回的 Stream 怎样正确关闭
- 下一篇
- Java Optional 为什么不适合字段类型
-
- 文章 · java教程 | 1小时前 | Java · 可观测性 · java Event Flight Recorder JFR Recording
- Java JFR 怎样记录自定义事件
- 456浏览 收藏
-
- 文章 · java教程 | 1小时前 | 文件处理 · Java · java Stream try-with-resources Files.walk
- Java Files.walk 返回的 Stream 怎样正确关闭
- 315浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java HttpClient 怎样分别限制连接建立与响应等待
- 189浏览 收藏
-
- 文章 · java教程 | 2小时前 | 并发 · Java · java synchronized 虚拟线程 JEP 491
- Java 虚拟线程为什么不适合长时间持有锁
- 161浏览 收藏
-
- 文章 · java教程 | 2小时前 |
- Java Stream 怎样保留分组后每组最大值
- 441浏览 收藏
-
- 文章 · java教程 | 2小时前 |
- Java record 怎样在紧凑构造器中规范化输入
- 435浏览 收藏
-
- 文章 · java教程 | 2小时前 |
- Java CompletableFuture 怎样合并两个异步结果
- 280浏览 收藏
-
- 文章 · java教程 | 16小时前 |
- AES/GCM 怎样处理 AEAD 标签
- 147浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 697次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 707次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 777次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 738次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 571次使用
-
- Java 时间格式兼容多个输入格式的方法
- 2026-10-18 501浏览
-
- Java try-with-resources 多个资源关闭顺序是什么
- 2026-09-10 501浏览
-
- 矩阵主副对角线快速定位技巧
- 2026-05-31 501浏览
-
- Java多态优化流程代码与行为分发改进
- 2026-05-26 501浏览
-
- JVM 类元数据双亲委派链表深度解析
- 2026-05-21 501浏览

