【问题标题】:Is it possible to make GC manage native object's lifetime?是否可以让 GC 管理本机对象的生命周期?
【发布时间】:2011-07-15 22:58:09
【问题描述】:

凭借 C++ 和 C# 经验以及一些 Java 知识,我现在开始了一个 Java+JNI (C++) 项目(Android,如果这很重要的话)。

我有一个本地方法,它创建一些 C++ 类并将指向它的指针作为 Java long 值(例如句柄)返回。然后从 Java 代码中调用的其他本​​地方法,使用句柄作为参数对这个类进行一些本地操作。 C++ 端不拥有对象,Java 端拥有。但是在当前的架构设计中,很难定义谁确切拥有该对象以及何时删除它。因此,让 Java VM 垃圾收集器以某种方式管理对象的生命周期可能会很好。 C++ 类不消耗任何资源,除了一些内存,不是很大。所以没关系,如果几个这样的对象不会被破坏。

在 C# 中,我可能会将本机 IntPtr 句柄包装在一些托管包装类中。并在托管包装器被垃圾收集时覆盖它的终结器以调用本机对象的析构函数。 SafeHandle、AddMemoryPressure 等可能在这里也有帮助。

这与 Java 的 finalize 不同。在 Java 中的“Hello world”之后,您知道的第二件事是使用 finalize 是不好的。有没有其他方法可以在 Java 中实现这一点?也许使用 PhantomReference?

【问题讨论】:

  • 在 C# 中使用终结器是“不好的”,原因与在 Java 中不好的原因相同 - 那么有什么区别?
  • 看看 BlueRaja 怎么说 ;-) C# 使用 IDisposable 而 Java 经常使用 Closable 接口。这些定义了生命周期的合同——但是是手动构造的。 (C# 支持using,Java7 将支持类似的东西。)C# 和 Java 的“问题”是使用的 GC(在当前实现中)不是引用计数并且是一个惰性标记/sweep hybrid——也就是说,为了一般的论证,GC的实际时间是“不确定的”。终结器或任何 *Reference 的问题在于 它们无法“看到”其他系统资源的压力
  • @pst:不,真正的问题是你不能保证终结器会在虚拟机关闭之前运行,你可以简单地解决这个问题(至少它是如何在 Java 中实现的) GC算法。而且我看不出对其他系统资源的压力与它有什么关系-如果对象仍在使用中,则无法释放它,并且如果它不是完整的 GC 将始终找到并释放它(现在 VM 没有'不关心任何事情,但记忆是真实的,但如果这是一个问题,那可以再次修复)
  • 正如我所说,我只有一点 Java 知识,所以我可能看不出为什么 finalize 在 Java 中“不好”的所有确切原因。至于C#,它不被认为是坏的。至少,例如,Richter 通过 C# 编写的 CLR 有一整章“使用终结化来释放本机资源”。此外,诸如 GC.AddMemoryPressure 或 HandleCollector 类之类的方法表明,.NET 团队实际上打算使用终结器来管理本机资源。
  • @Alex Che:没有读过那本书,但是如果你在终结器中处理资源,当操作系统关闭进程时不会自动释放(EXTREMELY 危险和糟糕的建议。 .NET 团队添加 IDisplosable 接口是有原因的,并不是“以不同的方式收集资源会很有趣”

标签: java memory-management garbage-collection java-native-interface native


【解决方案1】:

好吧,让我们考虑一下为什么 finalize 和 Co 存在问题:正如您所知,无法保证在 VM 关闭之前会调用 finalize,这意味着不一定会运行特殊的清理代码(imo 是一个错误的决定,我看不出在清理时通过 finalize 队列有任何问题,但就是这样)。这与 C# 中的情况完全相同

现在您的对象只消耗内存,当 VM 被销毁时,操作系统无论如何都会清理这些内存,因此最终确定存在问题的唯一情况对您来说无关紧要。所以是的,你确实可以使用这个变体,它会很好地工作,但它可能并不完全被认为是一个伟大的架构设计——一旦你将资源添加到操作系统无法正确处理清理的 C++ 代码中,你会遇到问题

还要注意,实现终结器会导致 GC 产生一些额外的开销,这意味着清理其中一个对象需要两个周期(无论您做什么,都不要在 finalize 方法中保存对象)

【讨论】:

  • 很公平。然而,由于 GC 独立于外部“压力”,它可能不会尽可能积极地释放对象(忽略可能永远不会调用终结器的事实)——这可能导致外部“资源不足”的情况资源被添加到组合中。一个示例是带有直接分配缓冲区的 ByteBuffer。
  • 谢谢你。所以我的问题是我不明白“最终覆盖是坏的”规则背后的所有确切原因。 Ratchet Freak 和 Konstantin Komissarchik 给出了基本相同的答案,但由于您的答案是第一个并且同时更详细,我接受它。
  • @pst,我创建了一个相关问题:stackoverflow.com/questions/6716582/…。可以合作吗?
【解决方案2】:

如果您了解为什么应该避免使用 Java 的 finalize 方法,您也将了解如何正确使用它。使用 finalize 关闭系统资源(文件和句柄)是不好的,因为您实际上不知道这些资源何时会被关闭和释放。使用复杂的终结逻辑是不好的,因为您的对象引用可能会泄漏并再次固定在内存中。

对于您的场景,使用 finalize 非常好。

【讨论】:

    【解决方案3】:

    在这里使用带有终结器的包装器是一个不错的解决方案

    但如果你真的不想这样做,你可以使用带有 ReferenceQueue 的 PhantomReference 来清理它(但你需要一个单独的线程来轮询队列)

    【讨论】:

      【解决方案4】:

      那么我们如何使用幻像引用来实现呢。

      1. 为您的本机 intPtr 对象创建一个包装器对象。创建一个 包装对象上的幻像引用(带有引用队列)。
      2. 创建和维护对 intPtr 的幻像引用的映射。
      3. 创建一个线程来监控引用队列的最终确定 包装对象实例。
      4. 此线程将从引用队列中获取幻像引用,使用幻像引用查找 intPtr 并在 intPtr 引用的本机 int 对象上调用析构函数。
      5. 当这一切发生时,您可以愉快地使用 您的 java 代码中的包装对象。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-10-21
        • 2012-11-06
        • 2012-02-18
        • 2012-12-28
        • 2020-07-28
        • 1970-01-01
        • 2012-10-14
        相关资源
        最近更新 更多