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

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

Why do you not explicitly call finalize() or start the garbage collector?

在阅读了这个问题之后,使我想起了什么时候教我Java的,并且告诉我不要调用finalize()或运行垃圾收集器,因为"这是一个大黑盒子,您无需担心"。有人可以将其理由简化为几句话吗?我确定我可以阅读Sun的有关此事的技术报告,但是我认为一个很好,简短的简单答案可以满足我的好奇心。


简短的答案:Java垃圾回收是一个非常精细的工具。 System.gc()是大锤。

Java的堆分为不同的世代,每个世代都是使用不同的策略收集的。如果将事件探查器附加到运行状况良好的应用程序,您会发现它很少需要运行最昂贵的集合,因为大多数对象是年轻一代中更快的复制收集器捕获的。

直接调用System.gc()虽然在技术上不能保证做任何事情,但实际上将触发昂贵的,停止运行的全堆收集。这几乎总是错误的做法。您认为您正在节省资源,但是实际上您没有充分理由浪费它们,迫使Java重新检查所有活动对象(以防万一)。

如果您在关键时刻出现GC暂停问题,最好将JVM配置为使用并发标记/清除收集器,该收集器专门设计用于最大程度地减少暂停所花费的时间,而不是尝试使用大锤。问题并进一步解决。

您正在考虑的Sun文档在这里:Java SE 6 HotSpot?虚拟机垃圾回收调整

(另一件事,您可能不知道:在您的对象上实现finalize()方法会使垃圾回收速度变慢。首先,需要运行两次GC来收集对象:一次运行finalize(),第二次运行以确保其次,具有finalize()方法的对象必须由GC视为特殊情况,因为它们必须单独收集,而不能只是大量丢弃。)


关于终结器:

  • 它们实际上是无用的。不能保证及时调用它们,或者根本不能保证它们被调用(如果GC从未运行,则终结器也不会)。这意味着您通常不应该依赖它们。
  • 终结器不保证是幂等的。垃圾收集器非常小心以确保它永远不会在同一对象上多次调用finalize()。对于编写良好的对象而言,这并不重要,但是对于编写不良的对象,多次调用finalize可能会导致问题(例如,双重释放本机资源...崩溃)。
  • 每个具有finalize()方法的对象也应提供一个close()(或类似方法)。这是您应该调用的函数。例如FileInputStream.close()。当您有一个打算由您调用的更合适的方法时,没有理由调用finalize()

  • 不要为终结器而烦恼。

    切换到增量垃圾收集。

    如果要帮助垃圾收集器,请将对不再需要的对象的引用无效。更少的路径=更明确的垃圾。

    请不要忘记(非静态)内部类实例保留对其父类实例的引用。因此,内部类线程保留的行李比您预期的要多得多。

    在非常相关的方面,如果您使用序列化,并且已经序列化了临时对象,则需要通过调用ObjectOutputStream.reset()来清除序列化缓存,否则您的进程将泄漏内存并最终死亡。
    缺点是非临时对象将被重新序列化。
    序列化临时结果对象可能比您想象的要麻烦一些!

    考虑使用软引用。如果您不知道什么是软引用,请阅读java.lang.ref.SoftReference

    的javadoc。

    除非真正感到兴奋,否则请避开幻影引用和弱引用。

    最后,如果您真的无法忍受GC,请使用Realtime Java。

    不,我不是在开玩笑。

    该参考实现可免费下载,SUN的Peter Dibbles的书非常好阅读。


    在finalize中关闭OS句柄的真正问题是finalize的执行没有保证的顺序。但是如果您能够处理阻塞的事物(例如套接字),则您的代码可能会陷入僵局(一点也不微不足道)。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    SomeStream s = null;
    ...
    try{
       s = openStream();
       ....
       s.io();
       ...
    } finally {
       if (s != null) {
           s.close();
           s = null;
       }
    }

    如果您编写自己的通过JNI和打开的句柄工作的类,它将变得更加复杂。您需要确保句柄已关闭(释放),并且它只会发生一次。在Desktop J2SE中经常被忽略的操作系统句柄是Graphics[2D]。甚至BufferedImage.getGrpahics()都可能使您返回指向视频驱动程序的句柄(实际上将资源保存在GPU上)。如果您不会自己释放它,而将其留给垃圾收集器来完成工作-当您用完显卡映射的位图但仍然有足够的内存时,您可能会发现奇怪的OutOfMemory和类似情况。以我的经验,它经常在处理图形对象的紧密循环中发生(提取缩略图,缩放比例,使其更清晰)。

    基本上,GC并不负责程序员对正确资源管理的责任。它只照顾内存,别无其他。最好通过将Stream.finalize调用close()恕我直言,抛出异常New RuntimeError("垃圾收集仍然打开的流")来实现。在草率的业余爱好者离开两端后,它将节省数小时和数天的调试和清理代码。

    编码愉快。

    和平。


    假设终结器与它们的.NET命名类似,那么仅当您拥有诸如文件句柄之类的资源时,才真正需要调用这些终结器。大多数情况下,对象没有这些引用,因此不需要调用它们。

    尝试收集垃圾是很糟糕的,因为它实际上不是您的垃圾。您已经告诉VM在创建对象时分配一些内存,并且垃圾收集器隐藏了有关那些对象的信息。在内部,GC正在对其分配的内存进行优化。当您手动尝试收集垃圾时,您不知道GC要保留和清除的内容,您只是在强迫它。结果,您弄乱了内部计算。

    如果您对GC在内部拥有更多的了解,那么您也许可以做出更明智的决定,但是您就错过了GC的好处。


    避免使用终结器。无法保证会及时调用它们。内存管理系统(即垃圾收集器)决定用终结器收集对象可能需要很长时间。

    许多人使用终结器来完成诸如关闭套接字连接或删除临时文件之类的操作。这样做会使您的应用程序行为无法预测,并与JVM将GC传递给对象的时间有关。这可能导致"内存不足"的情况,这不是由于Java Heap耗尽了,而是由于系统用尽了特定资源的句柄。

    要记住的另一件事是,在您的环境中引入对System.gc()的调用或此类锤子可能会显示出良好的效果,但不一定会转换为其他系统。并非每个人都运行相同的JVM,有很多,例如SUN,IBM J9,BEA JRockit,Harmony,OpenJDK等。此JVM都符合JCK(即经过正式测试的JVM),但是有很多加快速度的自由。 GC是每个人都大力投资的领域之一。使用锤子常常会破坏这种努力。


    GC在何时正确完成事情方面做了很多优化。

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


    推荐阅读