登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java reachabilityFence为什么放在资源使用后的finally里

来源:17golang原创

时间:2026-11-15 15:09:24 157浏览 收藏

Reference.reachabilityFence(owner) 放在资源使用之后,是为了让包装对象至少保持强可达直到这次同步使用结束;放进 finally,则让正常返回和异常离开都经过这个保护点。它避免的是因包装对象过早不再可达而触发自动清理,并不负责调用 close,也不能阻止其他线程显式关闭资源。

适合讨论的场景是:Java 对象保存一个外部资源句柄,Cleaner 根据包装对象的可达性兜底释放该资源。业务方法把句柄读到局部变量后,后续操作可能只需要这个数字;“方法还没返回”并不自动等于“包装对象仍然需要保持可达”。

Java 官方文档地址:https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/ref/Reference.html

明确需要保护的是资源包装对象

JLS 对可达性的讨论允许实现依据后续计算是否仍需要对象进行优化。局部变量在源码作用域内存在,并不表示对象一定会活到右花括号。对于普通纯内存对象,这通常没有可见影响;当可达性关联外部资源清理时,才需要认真处理这项约定。

可达性规范:https://docs.oracle.com/javase/specs/jls/se25/html/jls-12.html#jls-12.6.1

本文把资源包装对象称为 owner,把持有句柄的独立对象称为清理状态。它们的角色不同:清理状态还活着,不代表 owner 仍然强可达。fence 的参数应当是决定自动清理时机的那个 owner。

准备独立清理状态与资源所有权

先明确前提:同一个外部资源只有一个释放所有者;同步读取期间不允许其他线程显式 close;原生读取返回时已经停止使用资源。Cleaner 的清理动作不能反向持有被注册的包装对象,否则会破坏自动清理的可达性条件。

下面用 NativeBlock 展示结构。JNI读取和释放方法是集成点,需要真实原生库实现;示例没有提供资源分配器、JNI实现或运行结果,也不用于证明某次 GC 必然在某个位置发生。

import java.lang.ref.Cleaner;
import java.lang.ref.Reference;
import java.util.concurrent.atomic.AtomicLong;

// 结构示例:原生方法需由真实 JNI 库实现,本类不是独立可运行程序
final class NativeBlock implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();

    // 静态清理状态只保存句柄,不反向引用 NativeBlock
    private static final class ReleaseState implements Runnable {
        private final AtomicLong handle;

        ReleaseState(long ownedHandle) {
            handle = new AtomicLong(ownedHandle);
        }

        long requireHandle() {
            long value = handle.get();
            if (value == 0) {
                throw new IllegalStateException("资源已经关闭");
            }
            return value;
        }

        @Override
        public void run() {
            long value = handle.getAndSet(0);
            if (value != 0) {
                // 桥接层约定释放操作短小且不抛异常
                freeNative(value);
            }
        }
    }

    private final ReleaseState state;
    private final Cleaner.Cleanable cleanable;

    // 调用方转交一个有效非零句柄的唯一释放责任
    NativeBlock(long ownedHandle) {
        if (ownedHandle == 0) {
            throw new IllegalArgumentException("句柄不能为零");
        }
        state = new ReleaseState(ownedHandle);
        cleanable = CLEANER.register(this, state);
    }

    int readByte() {
        try {
            long handle = state.requireHandle();
            // 该调用必须同步完成使用,不能在返回后继续持有资源
            return readNative(handle);
        } finally {
            // 保活的是被 Cleaner 注册的包装对象,不只是句柄状态
            Reference.reachabilityFence(this);
        }
    }

    @Override
    public void close() {
        // 确定性释放仍由调用方负责
        cleanable.clean();
    }

    private static native int readNative(long handle);
    private static native void freeNative(long handle);
}

ReleaseState 使用静态嵌套类,避免隐式捕获外层实例;非零句柄交给该状态管理,零作为已关闭标记。AtomicLong 只帮助记录关闭状态,不自动使“检查后再使用”与 close 互斥。这个限制直接决定示例的适用范围。

Cleaner 官方说明:https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/ref/Cleaner.html

NativeBlock包装对象、清理状态与原生句柄的静态所有权关系
图1:原创结构图,包装对象持有清理状态,清理动作独立保存句柄;清理状态不反向引用包装对象。

在同步调用后的 finally 放置 fence

readByte 的 try 中完成资源使用,finally 中对 this 调用 fence。这样保活的是 Cleaner 注册时关联的 NativeBlock,而不是局部 long 句柄。如果读取抛出异常,finally 仍按 Java 的异常处理规则执行。

抽去业务细节后,所需结构如下。它是放置位置的片段,operation 与 owner 都要由实际类提供。

// 仅展示方法内的放置结构;operation 必须在返回前结束资源使用
try {
    operation();
} finally {
    // owner 是决定自动清理何时可以发生的包装对象
    Reference.reachabilityFence(owner);
}

不要把 fence 当成一次“刷新GC状态”的动作,也不要认为调用后对象立刻被回收。Reference 文档明确说明该方法本身不启动垃圾回收、引用处理或终结操作;它提供的是可达性保证。

方法说明:https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/ref/Reference.html#reachabilityFence(java.lang.Object)

理解异常路径与放错位置的差别

写法保护边界问题
先 fence 再使用资源保证只到前面的保护点后续使用没有因此获得保活保证
使用后普通语句调用 fence只覆盖走到该语句的路径此前异常离开时可能跳过
使用后的 finally 中调用覆盖正常与异常离开路径仍需满足同步使用及关闭约定
对清理状态而非 owner 调用保活了另一个对象未必保护 Cleaner 注册对象

finally 是为了覆盖控制路径;准确的保护对象与覆盖范围同样重要。若方法在另一层已经保证 owner 始终强可达,则不必机械地为每个普通Java方法添加 fence。

可达性保护、显式关闭和异步使用的责任边界
图2:原创说明图,fence、显式close与异步任务各有责任范围,图中连线表达归属而不是执行顺序。

另外处理 close 并发与异步资源使用

最容易过度推断的是“有了fence,资源访问就线程安全”。上面的 requireHandle 可能先读到非零值,另一线程随后执行 close,再由读取线程调用原生方法。对象此时完全可以仍然强可达,因此 fence 无法排除这种显式关闭竞态。

如果业务允许并发 close,需要另外设计互斥、读写锁或资源租约,让操作从获取使用权到释放使用权期间都受到保护。仅把句柄换成原子变量不能完成这件事。

异步任务也不能照搬同步方法的边界。原生接口若在返回后继续使用句柄,方法出口的 fence 结束得太早;应让 owner 或明确的租约覆盖任务真正结束、取消和失败清理的路径。任务提交成功不等于资源使用完成。

安排阅读检查与清理责任

把这类代码接入项目时,先确认四件事:fence的参数是否是被清理的包装对象;所有同步资源使用是否都位于保护点之前;异常路径是否经过finally;显式关闭是否由另一套生命周期协议控制。本文提供的是接入时的阅读检查项,没有执行代码或GC复现。

正常业务仍应通过 AutoCloseable 的确定性关闭方式释放资源,Cleaner只承担兜底角色。资源分配失败、注册失败和原生释放失败的处理应按桥接库约定补齐,不能把此处的结构示例当成完整生产级原生资源管理库。

是否可以用 System.gc() 检查 fence 是否生效?单次强制GC请求和日志先后不能证明所有优化场景,不能把偶然未复现当成生命周期正确性依据。

fence 是否可以替代 try-with-resources?不可以。前者处理可达性保证,后者表达离开作用域时关闭资源的责任。

只保存句柄到局部变量够不够?句柄值仍在使用,不一定意味着 owner 仍被需要。需要保护的是控制清理时机的对象,而不是只保留一个数字。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>