为什么不显式调用finalize()或启动垃圾收集器?

28
阅读完这个问题后,我想起当我学习Java时被告知永远不要调用finalize()或运行垃圾收集器,因为"它是一个你永远不需要担心的大黑匣子"。有人能简单地用几句话解释一下这样做的原因吗?我相信我可以从Sun的技术报告中了解到这个问题,但我认为一个简短、简单的答案就能满足我的好奇心。
7个回答

44
简短回答:Java垃圾回收是一个非常精细调整的工具,而System.gc()则是一个重锤。
Java堆被划分为不同的代,每个代使用不同的策略进行回收。如果你在一个健康的应用程序上附加了分析器,你会发现它很少需要运行最昂贵的那种垃圾回收,因为大多数对象都被年轻一代中较快的复制收集器捕获。
直接调用System.gc(),虽然从技术上讲不能保证任何操作,但在实践中,它几乎总是会触发一个昂贵的、停止所有线程的堆全局垃圾回收。这几乎总是错误的做法。你认为你在节省资源,但实际上你没有充分利用资源,只是“为了防备万一”强迫Java重新检查所有活动对象。
如果在关键时刻出现GC暂停问题,你最好配置JVM使用并发标记/清除收集器,该收集器专门设计用于最小化暂停时间,而不是试图用重锤来解决问题并进一步破坏它。
你想的Sun文档在这里:Java SE 6 HotSpot™ Virtual Machine Garbage Collection Tuning (另一件你可能不知道的事情:在你的对象上实现finalize()方法会使垃圾回收变慢。首先,它需要两遍GC运行来回收对象:一遍运行finalize()方法,下一遍确保对象在终结期间没有被重建。其次,具有finalize()方法的对象必须被GC视为特殊情况,因为它们必须被单独收集,不能批量丢弃。)

4
不要费心使用finalizers。
转向增量垃圾回收。
如果您想帮助垃圾收集器,请将不再需要的对象引用设置为null。路径越少= 显式垃圾。
不要忘记,(非静态)内部类实例保留对其父类实例的引用。因此,内部类线程保留的负担比您预期的要多得多。
在非常相关的情况下,如果您正在使用序列化,并且已经序列化了临时对象,则需要通过调用ObjectOutputStream.reset()来清除序列化缓存,否则您的进程将泄漏内存并最终死亡。缺点是非瞬态对象将被重新序列化。序列化临时结果对象可能会比您想象的更麻烦!
考虑使用软引用。如果您不知道软引用是什么,请阅读java.lang.ref.SoftReference的javadoc。
除非您真的很兴奋,否则请避开虚引用和弱引用。
最后,如果您真的无法容忍GC,请使用Realtime Java。
不,我不是在开玩笑。
参考实现可免费下载,SUN的Peter Dibbles书籍非常好读。

我认为WeakReference的使用情况比SoftReference更强。如果Foo需要引用Bar以使Foo受益,则应使用强引用。如果Foo需要引用Bar以使Bar受益,则应使用弱引用。例如,每次Foo执行某些操作时,Bar可能希望得到通知,但是如果Foo不必通知任何人,那么它也会很高兴。如果对Bar的唯一引用是由那些并不真正关心它是否存在的东西持有的,那么它就不应该存在。 - supercat

4

就终结器而言:

  1. 它们几乎没有用处。它们不能保证及时调用,或者根本不会被调用(如果垃圾回收从未运行,则终结器也不会运行)。这意味着你通常不应该依赖它们。
  2. 终结器不能保证幂等性。垃圾回收器非常小心地保证永远不会在同一对象上调用 finalize() 多次。对于写得好的对象来说,这没关系,但对于写得不好的对象来说,多次调用 finalize 可能会导致问题(例如,释放本机资源两次... 崩溃)。
  3. 每个具有 finalize() 方法的对象还应提供 close()(或类似)方法。这是你应该调用的函数。例如,FileInputStream.close()。当你有一个更合适的方法可以被你调用时,没有理由去调用 finalize()

1

在finalize中关闭操作系统句柄的真正问题在于finalize的执行顺序没有保证。但是,如果您有阻塞事物的句柄(例如套接字),那么您的代码可能会陷入死锁状态(这并不是微不足道的)。

因此,我赞成以可预测的有序方式显式地关闭句柄。基本上,处理资源的代码应该遵循以下模式:

SomeStream s = null;
...
try{
   s = openStream();
   ....
   s.io();
   ...
} finally {
   if (s != null) {
       s.close();
       s = null;
   }
}

如果您编写通过JNI和打开句柄工作的自己的类,情况会变得更加复杂。您需要确保句柄已关闭(释放),并且仅发生一次。在桌面J2SE中经常被忽视的操作系统句柄是Graphics[2D]。甚至BufferedImage.getGrpahics()也可能会返回指向视频驱动程序(实际上在GPU上持有资源)的句柄。如果您不释放它并让垃圾收集器来完成工作,您可能会发现奇怪的OutOfMemory等情况,当您用尽映射位图的视频卡但仍有大量内存时。在我个人的经验中,这种情况在处理图形对象的紧密循环中(提取缩略图、缩放、锐化等)经常发生。

基本上,GC不负责程序员正确管理资源的责任。它只负责内存,什么都不管。在我的看法中,Stream.finalize调用close()最好实现为抛出异常new RuntimeError("garbage collecting the stream that is still open")。这将节省数小时和数天的调试和清理代码,因为那些懒散的业余爱好者留下了松散的结尾。

愉快的编码。

和平。


1

假设终结器类似于它们的.NET名称,那么只有在您拥有可能泄漏的资源(如文件句柄)时才真正需要调用它们。大多数情况下,您的对象没有这些引用,因此不需要调用它们。

尝试收集垃圾是不好的,因为它并不是真正属于您的垃圾。您创建对象时已经告诉 VM 分配一些内存,而垃圾收集器隐藏了关于这些对象的信息。内部 GC 对其进行的内存分配进行了优化。当您手动尝试收集垃圾时,您对 GC 希望保留和丢弃什么没有任何了解,您只是在强制执行它的操作。结果会破坏内部计算。

如果您更了解 GC 内部所持有的内容,则可能能够做出更明智的决策,但这样一来,您就错过了 GC 的好处。


0

垃圾回收器在决定何时适当地完成任务方面进行了大量优化。

因此,除非您熟悉GC的实际工作原理以及如何标记代,否则手动调用finalize或启动GC可能会损害性能而非帮助。


0
避免使用finalizers。不能保证它们会及时被调用。在内存管理系统(即垃圾回收器)决定回收带有finalizer的对象之前,可能需要相当长的时间。
许多人使用finalizers来关闭套接字连接或删除临时文件等操作。这样做会使应用程序的行为变得不可预测,并与JVM何时对对象进行垃圾回收相关联。这可能导致“内存不足”情况,不是由于Java堆耗尽,而是由于系统对特定资源的句柄用尽。
还要记住的一件事是,引入对System.gc()等方法的调用可能在您的环境中显示出良好的结果,但不一定能转化到其他系统。并非每个人都使用相同的JVM,有很多种,如SUN、IBM J9、BEA JRockit、Harmony、OpenJDK等。这些JVM都符合JCK(经过官方测试的那些),但在提高性能方面有很大的自由度。垃圾回收是所有人都在大力投资的领域之一。使用“锤子”通常会破坏这种努力。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接