当前位置:首页 > 文章列表 > 文章 > java教程 > Java Cleaner 怎样注册可触发的兜底清理动作

Java Cleaner 怎样注册可触发的兜底清理动作

来源:17golang原创 2026-10-26 03:53:33 0浏览 收藏

如果一个 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);
    }
}
Cleaner、Resource、Cleanable 与独立清理状态之间的静态引用边界说明图
图1:注册关系结构说明图,重点看清理状态与业务对象的引用边界,不是运行截图。

这里有两个容易漏掉的细节。第一,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 上的其他清理动作。

AutoCloseable 显式 close、Cleanable.clean 与 GC 兜底的生命周期职责边界说明图
图2:生命周期边界说明图,比较显式清理与 GC 兜底的职责,不是执行流程或性能证据。
场景优先方案Cleaner 的位置
必须立刻释放的外部资源try-with-resources 或显式 close只做遗漏关闭的兜底
释放动作很短且只依赖独立句柄close 加 Cleaner适合作为第二道保险
清理需要阻塞或依赖业务对象显式生命周期管理不宜直接交给 Cleaner

最后再检查三个失效点:清理动作是否捕获了 this;Cleanable 是否一直保存;清理逻辑是否能在重复触发和异常被忽略的情况下保持安全。只要其中一项答不上来,就先回到显式资源管理,而不是继续增加 Cleaner 注册。

常见问题

Cleaner 会在对象离开作用域后马上执行吗?

不会。作用域结束只代表局部变量不再使用,Cleaner 还要等对象达到相应的可达性状态;因此它不能作为精确的释放时钟。

为什么清理动作不能写成普通内部类?

普通内部类通常隐含持有外层实例,清理动作就可能反向保留被注册对象。把状态类做成静态嵌套类,并只保存独立句柄,可以避开这个引用链。

调用 clean 两次会释放两次吗?

Cleanable.clean() 的动作最多执行一次;示例中的句柄置零又增加了一层防御,便于让资源释放逻辑具备幂等边界。

总结

Java Cleaner 的正确用法不是“把 close 交给 GC”,而是注册一个不持有业务对象的独立清理状态,并把 Cleanable 保存下来。正常路径调用 close() 和 clean(),意外遗漏时再让 Cleaner 兜底;遇到阻塞、强时效或依赖对象状态的清理任务,仍应坚持显式生命周期管理。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Java Files.walk 返回的 Stream 怎样正确关闭Java Files.walk 返回的 Stream 怎样正确关闭
上一篇
Java Files.walk 返回的 Stream 怎样正确关闭
Java Optional 为什么不适合字段类型
下一篇
Java Optional 为什么不适合字段类型
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    697次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    707次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    777次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    738次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    571次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码