【问题标题】:Out of memory errors in Java API that uses finalizers in order to free memory allocated by C callsJava API 中的内存不足错误,它使用终结器来释放 C 调用分配的内存
【发布时间】:2020-10-08 14:33:03
【问题描述】:

我们有一个 Java API,它是 C API 的包装器。 因此,我们最终得到了几个封装 C++ 类的 Java 类。

这些类实现 finalize 方法以释放已分配给它们的内存。

通常,这可以正常工作。但是,在高负载情况下,我们会出现内存不足异常。 内存转储表明几乎所有内存(在本例中约为 6Gb)都被终结器队列和等待终结的对象填充。

相比之下,C API 本身的内存使用量从未超过 150 Mb。

在低负载下,Java 实现可以无限期地运行。所以这似乎不是内存泄漏。似乎在高负载下,需要终结的新对象的生成速度比终结器的执行速度要快。

显然,“正确”的解决方法是减少正在创建的对象的数量。然而,这是一项重大的任务,需要一段时间。与此同时,是否有一种机制可以帮助缓解这个问题?例如,通过为 GC 提供更多资源。

【问题讨论】:

  • 这就像在问,你的枪和膝盖之间的枕头应该有多厚。围绕用 C++ 实现的东西创建 Java 包装器还不错,但是在 Java 类和 C++ 类之间建立一对一的对应关系是两全其美的结合。
  • @Holger 是的,我明白这一点,但事实就是如此。我想我希望有人可能有一个凯夫拉枕头,我可以在将膝盖移到更安全的地方时使用它。 :)
  • 使用direct ByteBuffer 可能会减少问题,因为实现会跟踪分配的本机内存量,并且能够减慢新分配的速度,直到处理完潜在的可回收缓冲区。当然,这完全取决于实现。

标签: java memory-management garbage-collection finalizer


【解决方案1】:

Java 的设计理念是终结器可以用作超出范围的对象的主要清理机制。当对象的总数足够小以至于“总是扫描所有东西”的垃圾收集器的开销是可以接受的时,这种方法可能几乎是可行的,但是相对较少的情况下,终结是系统中适当的清理措施带有分代垃圾收集器(几乎所有 JVM 实现都会有,因为与总是扫描所有内容相比,它提供了巨大的速度提升)。

只要可行,将Closable 与try-with-resources 结构一起使用是一种非常优越的方法。不能保证finalize 方法会以任何程度的及时性被调用,并且在许多情况下,相互关联的对象的模式可能会阻止它们被调用。虽然finalize 可用于某些用途,例如识别在持有资源的同时不正确被丢弃的对象,但它适合的用途相对较少。

如果你确实需要使用终结器,你应该了解一个重要的原则:与流行的看法相反,终结器不会在对象实际被垃圾回收时触发”——当对象将被垃圾回收时它们会触发但是在某处存在终结器 [包括但不限于对象自己的终结器]。当对它的任何引用存在于任何局部变量中时,实际上没有对象可以被垃圾收集,在任何其他对象中任何引用都存在,或者任何带有终结器的对象还没有运行完成。此外,为了避免在每个垃圾收集周期检查所有对象,已经存活一段时间的对象将被给予“免费通行证”大多数 GC 周期。因此,如果一个带有终结器的对象在被放弃之前还存活了一段时间,那么它的终结器可能需要相当长的时间才能运行,并且它将保留它持有引用的对象足够长的时间,以至于它们很可能也可以获得“免费通行证”。

因此,我建议尽可能,即使有必要使用终结器,您也应该将它们的使用限制在私有对象上,从而避免持有对其清理任务不明确需要的任何内容的强引用.

【讨论】:

  • 终结器从来没有打算成为“主要的清理机制”。从第一天开始,I/O 类就有 close() 方法。同样,windows 从 Java 1.0 开始就有一个dispose() 方法。最终确定始终是最后的手段,而不是主要的。并且“超出范围的对象”具有误导性。名称超出范围。对象可能变得无法访问,这只是松散地连接到变量范围。
  • @Holger:I/O 类必须处理立即关闭和重新打开特定文件的需要,这在完成时是不可能的,但我不知道“关闭”的概念已应用于占用有限但可替代资源的类。
  • 您想到了哪种资源和课程?据我所知,只有应该明确关闭的资源和只包含垃圾收集器将处理的内存的对象。如果有第三类,将终结器作为主要清理机制,我一定错过了那些实际用例。
  • @Holger:像 GDI 句柄之类的东西,以及需要在底层环境中使用不可重定位缓冲区的任何其他类型的对象。我对Java图形和窗口抽象不是很熟悉,但是在某些图形环境中,要求环境创建画笔然后将其用于许多图形操作可能比要求环境为每个操作创建画笔要快得多.同样,将图像加载到以特定于环境的方式保存的位图中并重复绘制它可能会快得多...
  • 没有人怀疑使用本机资源可能会加快图形操作。尽管如此,这些资源必须释放,最好以受控方式释放,而不是通过不可靠的最终确定。我在第一条评论中已经提到了Window.dispose()。还有Graphics.dispose() 等。您仍然没有说出“终结器可以用作主要清理机制”的示例。
【解决方案2】:

幻影引用是 Java 中可用的终结器的替代方案。

幻影引用让您可以更好地控制资源回收过程。

  • 您可以将显式资源处置(例如尝试使用资源构造)与 GC 基础处置相结合
  • 您可以使用多个线程进行事后整理

使用幻像引用非常复杂。在this article 中,您可以找到一个虚拟引用库资源管理的最小示例。

在现代 Java 中也有 Cleaner 类,它也基于幻像引用,但提供了易于使用的基础架构(引用队列、工作线程等)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-03-22
    • 2021-06-15
    • 1970-01-01
    • 2018-01-29
    • 1970-01-01
    • 1970-01-01
    • 2011-01-11
    • 1970-01-01
    相关资源
    最近更新 更多