为什么要实现finalize()?

我一直在阅读很多关于finalize()的新手Java问题,并发现没有人真正清楚地表明finalize()是一种不可靠的清理资源的方式,这有点令人困惑。我看到有人评论说他们用它来清理连接,这真的很可怕,因为唯一接近于保证连接关闭的方法是最后实现try (catch)。

我没有学过CS,但我已经用Java专业编程近十年了,我从来没有见过有人在生产系统中实现finalize()。这并不意味着它没有用处,或者和我一起工作的人一直在做正确的事情。

所以我的问题是,有什么用例实现finalize()不能更可靠地处理通过语言中的另一个进程或语法?

请提供具体的场景或您的经验,简单地重复Java教科书,或最终确定的预期用途是不够的,因为这不是这个问题的意图。

105071 次浏览

自1998年以来,我一直在专业地做Java,我从来没有实现过finalize()。一次也没有。

当编写将被其他开发人员使用的代码时,需要调用某种“清理”方法来释放资源。有时那些其他开发人员忘记调用您的清理(或关闭,或销毁,或其他)方法。为了避免可能的资源泄漏,您可以检查finalize方法,以确保该方法被调用,如果没有调用,您可以自己调用它。

许多数据库驱动程序在它们的Statement和Connection实现中这样做,以提供一点安全性,防止开发人员忘记调用close。

您不应该依赖finalize()来为您清理资源。Finalize()在类被垃圾回收之前不会运行。在使用完资源后显式地释放资源要好得多。

您可以将它用作持有外部资源(套接字、文件等)的对象的支持。实现一个close()方法和需要调用它的文档。

实现finalize()来执行close()处理,如果你检测到它还没有完成。可能会将一些东西转储到stderr,以指出你正在清理一个有错误的调用者。

它在异常/有bug的情况下提供了额外的安全性。并不是每个调用者每次都要做正确的try {} finally {}事情。不幸的是,但在大多数环境中都是如此。

我同意很少有人需要它。正如评论者指出的那样,它带来了GC开销。只有当你在一个长期运行的应用程序中需要“皮带和背带裤”的安全性时才使用。

我看到从Java 9开始,Object.finalize()已弃用!它们指向我们java.lang.ref.Cleanerjava.lang.ref.PhantomReference作为替代。

我在生产代码中使用finalize的唯一一次是执行一个检查,检查给定对象的资源是否已被清理,如果没有,则记录一个非常明显的消息。它实际上并没有尝试自己去做,如果做得不好,它就会大喊大叫。结果很有用。

小心你在finalize()中所做的事情。特别是当您使用它来调用close()以确保资源被清理时。我们遇到了几种将JNI库链接到正在运行的java代码的情况,在任何使用finalize()调用JNI方法的情况下,我们都会得到非常严重的java堆损坏。损坏不是由底层JNI代码本身引起的,本机库中的所有内存跟踪都是正常的。这只是我们从finalize()调用JNI方法的事实。

这是JDK 1.5,它仍然在广泛使用。

直到很久以后,我们才发现出了问题,但最终罪魁祸首始终是使用JNI调用的finalize()方法。

finalize()是对JVM的一个提示,它可能很好地执行你的代码在一个未指定的时间。当您希望代码莫名其妙地无法运行时,这是很好的方法。

在终结器中做任何重要的事情(基本上除了日志)在三种情况下也很好:

  • 您希望打赌其他已完成的对象仍然处于程序其余部分认为有效的状态。
  • 您希望向所有具有终结器的类的所有方法添加大量检查代码,以确保它们在终结后行为正确。
  • 您希望意外地复活已完成的对象,并花费大量时间试图弄清楚为什么它们不起作用,和/或为什么它们在最终释放时没有最终完成。

如果你认为你需要finalize(),有时你真正想要的是幻影参考(在给出的例子中,它可以保存对其引用者使用的连接的硬引用,并在幻影引用排队后关闭它)。这也有一个属性,它可能神秘地永远不会运行,但至少它不能调用已完成对象的方法或复活对象。所以当你不是绝对需要干净地关闭连接,但你很想这样做,而且你的类的客户端不能或不会调用close自己(这实际上是足够公平的——如果你设计的接口需要一个特定的操作在收集之前被执行,那么有一个垃圾收集器有什么意义呢?这让我们回到了malloc/free的时代。)

其他时候,你需要你认为你正在管理的资源来变得更强大。例如,为什么需要关闭该连接?它最终必须基于系统提供的某种I/O(套接字,文件,等等),那么为什么当最低级别的资源被gced时,你不能依靠系统为你关闭它呢?如果另一端的服务器绝对要求您干净地关闭连接,而不仅仅是断开套接字,那么当有人绊倒正在运行代码的机器的电源线,或者中间的网络中断时,将会发生什么?

免责声明:我以前在JVM实现上工作过。我讨厌终结者。

嗯,我曾经用它来清理没有返回到现有池的对象。

它们经常被转来转去,所以不可能知道什么时候能安全地放回泳池。问题在于,它在垃圾收集过程中带来了巨大的损失,远远超过了使用对象池节省的成本。在我拆除整个池,让所有内容都变得动态并完成之前,它已经投入了大约一个月的时间。

公认的答案是好的,我只是想补充的是,现在有一种方法来实现finalize的功能,而不实际使用它。

看看“Reference”类。弱引用,幻影引用&软引用。

您可以使用它们来保存对所有对象的引用,但是这个引用ALONE不会停止GC。这样做的好处是,当它被删除时,你可以让它调用一个方法,这个方法可以是保证

至于finalize: 我使用finalize一次来了解哪些对象正在被释放。你可以用静态、引用计数等来玩一些巧妙的游戏——但这只是为了分析,但要注意这样的代码(不仅仅是在finalize中,但这是你最有可能看到它的地方):

public void finalize() {
ref1 = null;
ref2 = null;
othercrap = null;
}

这表明某人不知道自己在做什么。像这样的“清理”实际上是不需要的。当类被GC化时,这将自动完成。

如果你在finalize中发现这样的代码,那肯定是编写它的人搞糊涂了。

如果它在其他地方,可能是代码是一个坏模型的有效补丁(一个类存在很长一段时间,由于某种原因,它引用的东西必须在对象GC之前手动释放)。一般来说,这是因为有人忘记删除监听器或其他东西,不知道为什么他们的对象没有被GC,所以他们只是删除它引用的东西,耸耸肩然后离开。

它永远不应该被用来“更快”地清理东西。

一个简单的规则:永远不要使用终结器。

对象具有终结器(不管它执行什么代码)这一事实本身就足以导致相当大的垃圾收集开销。

来自Brian Goetz的文章:

带有终结器的对象(那些 有一个非平凡的finalize()方法) 有显著的开销相比 对象没有结束符,并且应该 要谨慎使用。Finalizeable 对象的分配都比较慢 收集的速度也更慢。在分配 时间,JVM必须注册任何时间 带有垃圾的可终结对象 收集器,并且(至少在 热点JVM实现) 可终结对象必须遵循 比大多数其他分配路径更慢 对象。同样,finalizeable 对象的收集速度也较慢。它 至少需要两次垃圾回收 循环(在最好的情况下) 可终结对象可以回收, 而垃圾收集器必须做的事情 调用终结器的额外工作。 结果是花费了更多的时间 分配和收集对象和 垃圾压力更大 收集器,因为内存被占用 不可到达的可终结对象是 保留更长的时间。把它和 事实上终结器不是 保证在任何可预测的情况下运行 时间框架,甚至根本,你可以 可以看到相对较少 终结的情况

.使用正确的工具

为了强调以上答案中的一点:终结器将在单独的GC线程上执行。我听说过一个大型的Sun演示,开发人员在一些终结器中添加了一个小睡眠,并故意让一个原本花哨的3D演示崩溃。

最好避免,可能的例外是test-env诊断。

Eckel's Thinking in Java有一个好的部分关于这一点。

我不知道你是怎么想的,但是…

itsadok@laptop ~/jdk1.6.0_02/src/
$ find . -name "*.java" | xargs grep "void finalize()" | wc -l
41

所以我猜太阳发现一些的情况下(他们认为)它应该被使用。

finalize()可以用来捕捉资源泄漏。如果资源应该关闭,但没有关闭,则将资源未关闭的事实写入日志文件并关闭。通过这种方式,您可以删除资源泄漏,并提供一种方法来知道它已经发生,以便您可以修复它。

我从1.0 alpha 3(1995)开始就一直在用Java编程,我还没有覆盖任何东西的finalize…

class MyObject {
Test main;


public MyObject(Test t) {
main = t;
}


protected void finalize() {
main.ref = this; // let instance become reachable again
System.out.println("This is finalize"); //test finalize run only once
}
}


class Test {
MyObject ref;


public static void main(String[] args) {
Test test = new Test();
test.ref = new MyObject(test);
test.ref = null; //MyObject become unreachable,finalize will be invoked
System.gc();
if (test.ref != null) System.out.println("MyObject still alive!");
}
}

====================================

结果:

This is finalize


MyObject still alive!

=====================================

所以你可以在finalize方法中使一个不可达的实例可达。

编辑:好吧,这真的不管用。我实现了它,并认为如果它有时失败了,这对我来说是可以的,但它甚至没有调用finalize方法一次。

我不是一个专业的程序员,但在我的程序中,我有一个案例,我认为是一个使用finalize()的好例子,那是一个缓存,在它被销毁之前将其内容写入磁盘。因为它没有必要在每次销毁时都执行,它只会加速我的程序,我希望我没有做错。

@Override
public void finalize()
{
try {saveCache();} catch (Exception e)  {e.printStackTrace();}
}


public void saveCache() throws FileNotFoundException, IOException
{
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("temp/cache.tmp"));
out.writeObject(cache);
}

删除已经添加到全局/静态位置(不需要)的东西,并在对象被删除时需要删除,这是很方便的。例如:

private void addGlobalClickListener() {
weakAwtEventListener = new WeakAWTEventListener(this);


Toolkit.getDefaultToolkit().addAWTEventListener(weakAwtEventListener, AWTEvent.MOUSE_EVENT_MASK);
}


@Override
protected void finalize() throws Throwable {
super.finalize();


if(weakAwtEventListener != null) {
Toolkit.getDefaultToolkit().removeAWTEventListener(weakAwtEventListener);
}
}

iirc——你可以使用finalize方法作为实现昂贵资源池机制的一种手段——这样它们就不会得到GC了。

接受的答案列出了在finalize期间关闭资源的操作。

然而,这个答案表明至少在java8的JIT编译器中,你会遇到意想不到的问题,有时甚至在你完成从对象维护的流中读取之前就调用了终结器。

因此,即使在这种情况下,也建议调用

就我个人而言,我几乎从未使用过finalize(),除非在一种罕见的情况下:我创建了一个自定义泛型类型集合,并编写了一个自定义finalize()方法,它执行以下操作:

public void finalize() throws Throwable {
super.finalize();
if (destructiveFinalize) {
T item;
for (int i = 0, l = length(); i < l; i++) {
item = get(i);
if (item == null) {
continue;
}
if (item instanceof Window) {
((Window) get(i)).dispose();
}
if (item instanceof CompleteObject) {
((CompleteObject) get(i)).finalize();
}
set(i, null);
}
}
}

(CompleteObject是我创建的一个接口,它允许你指定你已经实现了很少实现的Object方法,如#finalize()#hashCode()#clone())

因此,使用姐妹#setDestructivelyFinalizes(boolean)方法,使用我的集合的程序可以(帮助)保证销毁对这个集合的引用也会销毁对其内容的引用,并处理任何可能在无意中保持JVM活动的窗口。我也考虑过停止任何线程,但这打开了一个全新的蠕虫罐头。

资源(文件,套接字,流等)需要关闭一旦我们完成他们。它们通常有close()方法,我们通常在try-catch语句的finally部分调用该方法。有时finalize()也可以被少数开发人员使用,但在我看来,这不是一个合适的方式,因为不能保证finalize总是被调用。

在Java 7中,我们有try-with-resources语句,可以像这样使用:

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
// Processing and other logic here.
} catch (Exception e) {
// log exception
} finally {
// Just in case we need to do some stuff here.
}

在上面的例子中,try-with-resource将通过调用close()方法自动关闭资源BufferedReader。如果愿意,我们也可以在自己的类中实现Closeable,并以类似的方式使用它。在我看来,这似乎更整洁,更容易理解。

作为旁注:

覆盖finalize()的对象由垃圾回收器进行特殊处理。通常,在对象不再在作用域中后,在收集周期中立即销毁对象。但是,可终结对象被移到队列中,在队列中,独立的终结线程将耗尽队列并在每个对象上运行finalize()方法。一旦finalize()方法终止,对象就可以在下一个循环中进行垃圾收集了。

来源:Finalize()在java-9上已弃用