Java基础知识之 使用 Cleaner 替代 finalize

前言

说到 Java 对象的清理,一定离不开 finalize 方法。不过在 Java 9 之后,这个方法已经被标记为废弃,取而代之的是 java.lang.ref.Cleaner。本文将先介绍 finalize 的使用场景和使用方式,然后再过度到 Cleaner 的使用上。

由于这种 Java 对象的清理,在工作中还是蛮常用的,因此掌握此技能还是挺必要的。阅读完本文,大家一定能掌握 Cleaner 的使用方法。

另外,这个是我的微信公众号,希望大家能够多多关注,我会不定期更新优秀的技术文章:

接下来,咱们就开始正文吧。

Object.finalize 方法

在 Java 中,一个对象如果不再使用,那么它就会在 JVM 垃圾回收时,进行析构释放该对象占用的内存空间。但如果这个对象持有了一些其他需要进行额外处理的资源(非堆内存资源),那么就得考虑这些资源的释放了。

在 Object 中,有一个方法 finalize,就是用来做这种操作的。这个方法会在对象被 GC 回收之前被 JVM 调用,一般都使用覆写这个函数来完成一些额外资源的清理工作,例如清理相关的 native 资源或是其他资源(socket、文件)的释放。

在这里推荐大家去看一下这篇文章,不仅讲解了 Object.finalize 方法,还讲解了 Object 中的其他所有方法:一文掌握 Object 类里的所有方法(wait、notify、finalize)

如果子类重写 finalize 这个方法,那么必须调用 super.finalize(),而且应该使用 try-finally 语法以确保始终调用 super.finalize()。下面就是一个使用 finalize 进行资源清理的例子:

class FinalizeObj {

    private long nativePointer;

    public FinalizeObj() {
        nativePointer = createNative();
    }

    @Override
    protected void finalize() throws Throwable {
        try {    
            // 清理工作
            super.finalize();
            releaseNative(nativePointer);
            nativePointer = 0L;
        } finally {
            super.finalize();        
        }
    }

    private native long createNative();
    private native void releaseNative(long nativePointer);
}

这个例子中,在构造方法中创建了 native 底层资源,并在 finalize 方法中释放 native 底层资源。

当一个对象覆写 finalize 方法后,其回收的流程如下:

当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了 finalize 方法,若未覆盖,则直接将其回收。否则,若对象未执行过 finalize 方法,将其移动到一个队列里,由一个低优先级线程执行该队列中对象的 finalize 方法。执行 finalize 方法完毕后,这些对象才成为真正的垃圾,等待下一轮垃圾回收。

但是现在想一想,这种进行资源清理的方式好吗?其实是不好的。例如程序员在覆写 finalize 方法时出现一些线程死锁的操作,那么就有可能造成垃圾回收的失败,同事也会产生严重的盐城阻塞问题。而且这个方法被执行的不确定性太大,一个对象从不可达到 finalize 方法被执行,完全依赖 JVM,这就无法保证被对象占用的资源被及时回收。

也是出于以上的原因,这个方法在 Java 9 中已经被标记为 Deprecated。那替代方案是啥呢?案是使用 java.lang.ref.Cleaner,这是 Java 9 推出的一个轻量级垃圾回收机制。

java.lang.ref.Cleaner

首先要说的是,这个类是 Java 9 的新特性,但是新特性往往都会带来繁琐的使用方式。但主要包含以下几个步骤

  1. 创建一个静态内部类继承 Runnable 作为进行清理的线程,在 run 方法中进行垃圾清理(必须使用 static 以避免持有外部实例的引用)
  2. 创建静态变量 cleaner 初始化为 Cleaner.create()
  3. 在构造方法中创建清理线程,并通过静态变量进行注册 cleaner.register,并保存返回的 cleanable
  4. 实现 AutoCloseable 接口,覆写 close 方法,在其中调用 cleanable.clean()

说起来比较麻烦,我们将上面那个例子改写一下,使用 Cleaner 来实现:

public class CleanerObj implements AutoCloseable {

    // 静态变量 cleaner 用于此类的实例的清理工作
    private static final Cleaner cleaner = Cleaner.create();
    // 静态内部类,清理线程,用于清理本类的实例
    private static class CleanRunnable implements Runnable {    

        private final long nativePointerForClean;

        CleanRunnable(long nativePointer) {
            //构造方法应该拿到用于清理的所有信息
            this.nativePointerForClean = nativePointer;
        }

        @Override
        public void run() {
            //在此进行清理操作
            releaseNative(nativePointerForClean);
        }
    }

    private long nativePointer;                   // native资源指针
    private final CleanRunnable cleanRunnable;    // 此对象的垃圾回收线程
    private final Cleaner.Cleanable cleanable;    // 注册时返回的 cleanable 对象

    public CleanerObj() {
        //创建底层资源,保存返回的指针
        this.nativePointer = createNative();    
        //将底层资源的指针传递给清理线程对象,以便进行清理操作
        this.cleanRunnable = new CleanRunnable(this.nativePointer);
        //通过静态变量cleaner注册此对象和清理线程对象
        this.cleanable = cleaner.register(this, cleanRunnable);
    }

    @Override
    public void close() {    //覆写AutoCloseable 的 close 方法
        cleanable.clean();    //调用 cleaner.register 返回的 cleanable 对象的 clean 方法
    }

    private native static long createNative();
    private native static void releaseNative(long nativePointer);
}

大家注意代码中的注解,已经解释的很清楚。这里再啰嗦一下,就是先创建属于类的静态变量 cleaner 和 清理线程,用于此类的对象的清理操作。而针对每一个对象,都需要通过这两个静态变量,创建清理线程并注册。最后在 AutoCloseableclose 方法中,调用 cleanable.clean()

实现 AutoCloseable 接口这使得此类在 try-finally 时能够自动调用 close 方法进行清理。如果未调用 close 方法,当这个对象不被任何地方引用时,Cleaner 也将调用清理操作。

我们先使用 try-finally 来进行测试:

try (CleanerObj obj = new CleanerObj()) {
    // do something with obj.
}
System.gc();

清理的流程如下:

System.out                I  createNative... thread = main
System.out                I  close... thread = main
System.out                I  CleanRunnable.run... thread = main
System.out                I  releaseNative... thread = main

当我们不使用 try-catch 时:

CleanerObj obj = new CleanerObj();
System.gc();

清理的流程如下:

System.out                I  createNative... thread = main
System.out                I  CleanRunnable.run... thread = Cleaner-0
System.out                I  releaseNative... thread = Cleaner-0

可以看到,两种方式最终都会调用到 releaseNative 方法进行垃圾清理,而不同的仅仅是垃圾处理的线程而已。

相比于 finalize 方法,使用 Cleaner 进行清理操作实在是太麻烦了。但这也是没办法的事情,随着后续 Java 技术的发展,很多传统类结构的设计一定要有所改变,因为现在的程序编写得越来越庞大,同时业务的繁琐程度也越来越高,以及硬件和网络通讯水平的不断发展,程序语言内部的结构升级必然是整个行业的趋势。

最后,这里通过一张图来演示 Java 中对象的生命周期,并结束本篇文章,祝大家升职加薪。

  • 39
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李斯维

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值