#夏日挑战赛# finalize移除在即,你用什么清理资源? 原创

发布于 2022-7-7 15:26
浏览
0收藏

本文正在参加星光计划3.0–夏日挑战赛

0. 学完本文你将会学到

  • finalize()方法是什么
  • 三种清理资源方案以及优缺点

1. 前言

JEP 421中,JDK 18已经明确表示将会移除finalize()方法。

#夏日挑战赛# finalize移除在即,你用什么清理资源?-开源基础软件社区

2. finalize是什么?

在跟finalize说bye bye并且找到它的代替品之前,让我们先来了解下finalize到底是什么?

finalize()方法是Object类中提供的一个方法,在GC(Garbage Collection)准备释放对象所占用的内存空间之前,它将会首先调用对象的finalize()方法。

在Java中,finalize()方法主要用来释放非资源(比如打开的文件资源、数据库连接等)。

Java中的每一个对象都有一个finalize()方法,每个对象可以参与到关闭资源的机制中来。这也解决了抛出异常以及其他清理代码可能会被遗漏的情况——只要在你的资源消耗的对象上覆盖finalize()方法就行了。

这个想法不错,但是实际又是完全另一回事,finalize有很多让人非常头疼的缺点。如下所示:

  • finalize()方法的调用时机具有不确定性,它可能永远也不会调用,从一个对象变得不可到达开始,到finalize()方法被执行,所花费的时间是难以预计的。而且因为Java的垃圾回收器的特性,我们不能依赖finalize()方法能及时地回收占用的资源。可能出现的情况是在我们耗尽资源之前,GC仍未触发,因此我们常常手动close。
  • finalize()方法可以误使本来已经死亡的类复活。设想这样一个情况,一个对象引发了一个异常,使它有进入GC。然而,finalize()方法首先得到运行的机会,该方法可以做任何事情,包括重新建立对该对象的实时引用。这是一个潜在的泄漏和安全的隐患。
  • finalize()方法是很难正确实现的。如果只是写一个看似可靠的finalize()方法,那么不能保证其想要实现的功能。特别是对于finalize()方法的线程影响,难以保证。
  • finalize()方法存在一定的性能问题。因为finalize()方法在实现它既定目的时不可靠,所以JVM在支持它上面所花费的开销是不值得的。
  • finalize()方法使得大规模应用更加脆弱,并且在遇到严重负载的情况下会出现难以恢复的错误情况。

3. finalize()方法的替代方案

让我们回顾下处理异常以及关闭资源的常用操作。

这里准备了三种替代方案:

  • try-catch-finally
  • try-catch-resource
  • cleaner

3.1 try-catch-finally

这个释放资源的方式应该是很常见的了。它在很多情况下是可行的,但是它也会有缺点——容易出错并且冗长。

关闭资源的时候有可能也会引发另外一个异常。在一个长期运行和重度使用的系统中,这种情况会产生资源泄露,从而扼杀一个应用程序。

而且这种粗暴的做法我们不得不在整个代码库中重复。

请看下面这个例子:

FileOutputStream outStream = null;
try {
    outStream = new FileOutputStream("output.txt");
    ObjectOutputStream stream = new ObjectOutputStream(outStream);
    . . .
    stream.close();
} catch(FileNotFoundException fe) {
    System.out.println("Can't find the file");
} catch(IOException ioe) {
    System.out.println("Can't write to file");
} finally {
    if (outStream != null) {
        try {
            outStream.close();
    } catch (Exception e) {
        System.out.println("Can't close the stream", e);
    }
  }
}

在这个例子里面,你要做的就是打开一个流,向它写进一些字节,然后保证无论抛出什么异常,它被关闭即可。

为了做到这一点,你不得不把这些代码放在一个try代码块中,然后在catch代码块中处理异常。你还需要添加一个finally块,对流进行double check。为了保证一个可能的异常不会阻止流的关闭,你不能直接关闭流,你还要用另一个try块将其包裹。

听上去就很繁琐,不是吗?就算你会熟练使用ctrl c和ctrl v,难免也会感到困惑。

这对一个简单而且普遍的需求来说,这么冗长啰嗦的代码确实让人心烦。

所以我们继续往下看看其他的解决方案。

3.2 try-with-resource

try-with-resource是在Java 7中引进的,所以对仍在使用JDK8的大多数用户来说,忘掉try-catch-finally吧。

try-with-resource语句允许你指定一个或多个资源对象作为try声明的一部分。这些资源保证在try块完成时被关闭。

更进一步来说,任何实现java.lang.AutoCloseable的类都可以用在try-with-resource语句中。

现在让我们用try-with-resource重写上一小节的例子,请看:

try (FileOutputStream outStream = new ObjectOutputStream(outStream)) {
    ObjectOutputStream stream = new ObjectOutputStream(outStream);
    stream.write //…
    stream.close();
} catch(FileNotFoundException fe) {
    System.out.println("Can't find the file");
} catch(IOException ioe) {
    System.out.println("Can't write to file");
}

显而易见,代码量减少了。但是最大的好处是,一旦你将try块括号内声明资源交给JVM,你将不会再担心它会产生资源泄露。

在这个例子中,我们已经消除了finally块。然后在有些情况下,我们需要一个更加强大的解决方案。

当情况更为复杂而这样单一的代码块无法满足我们需求时,我们需要一个Cleaner。

3.3 Cleaner

Cleaner类是在Java 9中引入的,用于管理一组对象引用和相应的清理操作。

Cleaner的想法是将清理程序与需要清理的对象的代码脱钩。

让我们看看这个Oracle文档中的例子:

public class CleaningExample implements AutoCloseable {
    // A cleaner, preferably one shared within a library
    private static final Cleaner cleaner = <cleaner>;
    static class State implements Runnable {
    State(...) {
      // initialize State needed for cleaning action
    }
    public void run() {
      // cleanup action accessing State, executed at most once
    }
  }
    private final State;
    private final Cleaner.Cleanable cleanable
    public CleaningExample() {
        this.state = new State(...);
        this.cleanable = cleaner.register(this, state);
    }
    public void close() {
        cleanable.clean();
    }
}

与finalize()方法不同的是,你可以明确地调用close()方法来清理你的对象引用。而finalize()方法完全依赖于来自GC的不确定性的调用。

如果没有明确地调用close()方法,作为cleaner.register()方法的第一个参数成为幻象可达的状态时,系统将为你执行它。然而,如果明确地执行了close()方法,系统就不会再调用它。

本节中的代码产生了一个AutoCloseable对象。这表示它可以被传递到try-with-resource语句的参数中。

现在我们使用Cleaner还要注意的是,不要在Cleaner的运行方法中创建已清理对象的引用,因为这样做会创建一个僵尸对象。

此外,我们可以看到cleaner是static的,因为每个Cleaner都会产生一个线程,所以共享Cleaner将导致运行程序的开销降低。

此外,我们还可以注意到被监控的对象与执行清理工作的代码(在本例中是State)是解耦的。

关于Cleaner机制,《Java编程思想》提倡避免使用它。认为它同样在不确定性和性能问题上存在缺陷,这点在本文中不再讨论,感兴趣的读者可移步此处

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
标签
已于2022-7-7 15:34:11修改
5
收藏
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐