读完这个问题后,我想起了当我教 Java 时,被告知永远不要调用 finalize() 或运行垃圾收集器,因为“这是一个你永远不需要担心的大黑匣子”。有人可以将其推理归结为几句话吗?我确信我可以阅读 Sun 关于这个问题的技术报告,但我认为一个不错的、简短的、简单的答案会满足我的好奇心。
7 回答
简短的回答:Java 垃圾收集是一个非常精细的工具。System.gc() 是一把大锤。
Java 的堆被划分为不同的代,每一代使用不同的策略收集。如果您将分析器附加到一个健康的应用程序,您会发现它很少需要运行最昂贵的集合类型,因为大多数对象都被年轻代中更快的复制收集器捕获。
直接调用 System.gc(),虽然技术上不能保证做任何事情,但实际上会触发昂贵的、停止世界的完整堆收集。这几乎总是错误的做法。你认为你在节省资源,但实际上你是在无缘无故地浪费它们,迫使 Java 重新检查你所有的活动对象“以防万一”。
如果您在关键时刻遇到 GC 暂停问题,最好将 JVM 配置为使用并发标记/清除收集器,该收集器专门设计用于最大限度地减少暂停时间,而不是试图对问题采取大锤并只是进一步打破它。
您正在考虑的 Sun 文档在这里:Java SE 6 HotSpot™ Virtual Machine Garbage Collection Tuning
(您可能不知道的另一件事:在对象上实现 finalize() 方法会使垃圾回收变慢。首先,需要两次GC 运行来收集对象:一次运行 finalize(),下一次运行以确保对象被'在终结期间不会复活。其次,具有 finalize() 方法的对象必须被 GC 视为特殊情况,因为它们必须单独收集,不能只是批量丢弃。)
不要打扰终结者。
切换到增量垃圾收集。
如果您想帮助垃圾收集器,请取消对不再需要的对象的引用。更少的路径=更明确的垃圾。
不要忘记(非静态)内部类实例保留对其父类实例的引用。因此,内部类线程保留的包袱比您预期的要多得多。
在一个非常相关的情况下,如果您正在使用序列化,并且您已经序列化了临时对象,您将需要通过调用 ObjectOutputStream.reset() 清除序列化缓存,否则您的进程将泄漏内存并最终死亡。缺点是非瞬态对象将被重新序列化。序列化临时结果对象可能比您想象的要复杂一些!
考虑使用软引用。如果您不知道什么是软引用,请阅读 java.lang.ref.SoftReference 的 javadoc
除非您真的很兴奋,否则请避开 Phantom 引用和 Weak 引用。
最后,如果你真的不能容忍 GC,请使用 Realtime Java。
不,我不是在开玩笑。
参考实现可免费下载,SUN 的 Peter Dibbles 书非常适合阅读。
就终结者而言:
- 它们几乎没有用。它们不能保证被及时调用,或者根本不能保证(如果 GC 永远不会运行,任何终结器也不会)。这意味着您通常不应该依赖它们。
- 终结器不保证是幂等的。垃圾收集器非常注意保证它不会
finalize()
对同一个对象多次调用。对于写得好的对象,这无关紧要,但对于写得不好的对象,多次调用 finalize 可能会导致问题(例如,本机资源的双重释放......崩溃)。 - 每个拥有
finalize()
方法的对象也应该提供一个close()
(或类似的)方法。这是您应该调用的函数。例如,FileInputStream.close()
。finalize()
当您有更合适的方法打算由您调用时,没有理由调用。
假设终结器与它们的 .NET 同名类似,那么您只有在拥有可能泄漏的文件句柄等资源时才真正需要调用它们。大多数情况下,您的对象没有这些引用,因此不需要调用它们。
尝试收集垃圾是不好的,因为它不是真正的垃圾。您在创建对象时告诉 VM 分配一些内存,而垃圾收集器正在隐藏有关这些对象的信息。在内部,GC 正在对其进行的内存分配进行优化。当您手动尝试收集垃圾时,您不知道 GC 想要保留和摆脱什么,您只是在强迫它。结果你搞砸了内部计算。
如果您对 GC 内部持有的东西有更多了解,那么您可能能够做出更明智的决定,但您已经错过了 GC 的好处。
在 finalize 中关闭 OS 句柄的真正问题是 finalize 的执行顺序没有保证。但是,如果您对阻塞的事物(例如套接字)有句柄,则您的代码可能会陷入死锁情况(根本不是微不足道的)。
所以我支持以可预测的有序方式显式关闭句柄。基本上处理资源的代码应该遵循以下模式:
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("垃圾收集仍然打开的流") 会更好地实现。在草率的业余爱好者失去目标后,它将节省数小时和数天的调试和清理代码。
快乐编码。
和平。
GC 对何时正确完成事情做了很多优化。
因此,除非您熟悉 GC 的实际工作方式以及它如何标记世代,否则手动调用 finalize 或 start GC'ing 可能会损害性能而不是帮助。
避免使用终结器。无法保证他们会及时被调用。在内存管理系统(即垃圾收集器)决定使用终结器收集对象之前可能需要很长时间。
许多人使用终结器来执行诸如关闭套接字连接或删除临时文件之类的操作。通过这样做,您可以使您的应用程序行为不可预测,并与 JVM 何时对您的对象进行 GC 相关联。这可能导致“内存不足”的情况,不是因为 Java 堆被耗尽,而是因为系统耗尽了特定资源的句柄。
要记住的另一件事是,引入对 System.gc() 或此类锤子的调用可能会在您的环境中显示出良好的结果,但它们不一定会转化为其他系统。不是每个人都运行同一个JVM,有很多,SUN、IBM J9、BEA JRockit、Harmony、OpenJDK等等……这个JVM都符合JCK(官方测试过的就是那个),但是有很多让事情变得更快的自由。GC 是每个人都大力投资的领域之一。使用锤子通常会破坏这种努力。