#夏日挑战赛# finalize移除在即,你用什么清理资源? 原创
0. 学完本文你将会学到
- finalize()方法是什么
- 三种清理资源方案以及优缺点
1. 前言
在JEP 421中,JDK 18已经明确表示将会移除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编程思想》提倡避免使用它。认为它同样在不确定性和性能问题上存在缺陷,这点在本文中不再讨论,感兴趣的读者可移步此处。
学习了~
学习了~
写得很好呀 求关注
学到了 写得很不错~
感谢作者 带来很好的文章