能否让垃圾回收器管理本地对象的生命周期?

6

我有C++和C#的经验,还略懂一点Java,现在开始进行一个Java+JNI(C++)项目(如果是Android平台则更好)。

我有一个本地方法,它创建了一些C++类,并将其作为Java long值(称为句柄)返回指针。然后其他从Java代码调用的本地方法使用该句柄作为参数,在这些类上执行一些本地操作。C++端不拥有该对象,而是由Java端拥有。但在当前的架构设计中,很难定义谁确切地拥有对象以及何时删除它。因此,让Java虚拟机垃圾回收程序以某种方式来管理对象的生命周期可能会很不错。C++类没有消耗任何资源,除了一小段内存,因此如果不销毁几个这样的对象也是可以的。

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

在Java中,情况就不同了。在Java的“Hello world”之后,你知道的第二件事就是finalize的使用是不好的。在Java中还有其他的方法来完成这个任务吗?或者可以使用PhantomReference?


1
@pst:真正的问题是你不能保证在虚拟机关闭之前一定会运行终结器,并且你可以为每个GC算法轻松解决这个问题(至少在Java中实现的方式如此)。我不明白其他系统资源的压力与此有什么关系——如果对象仍在使用中,你无法释放它,如果它没有被完全回收,GC将始终找到并释放它(现在虚拟机只关心内存,但如果这是一个问题,那也可以解决)。 - Voo
1
正如我所说,我只有一点Java知识,所以可能无法看到finalize在Java中为什么“不好”的所有确切原因。至于C#,它并不被认为是不好的。例如,Richter的CLR via C#就有一个完整的章节“使用Finalization释放本机资源”。此外,像GC.AddMemoryPressure或HandleCollector类这样的方法表明,.NET团队实际上打算使用finalizers来管理本机资源。 - Alex Che
作为一名C++程序员,我早在C#引入IDisposable模式之前就已经学习了它(参见RAII)。我喜欢这个模式,但它只适用于简单的资源管理情况,即对象的生命周期不会超出代码块的范围。 - Alex Che
@Voo,没错,但在我的情况下,它只是一种内存,在进程关闭时会自动释放。 - Alex Che
@Alex,这就是为什么我和其他几个人已经说过,在你的情况下这不是一个问题。 - Voo
显示剩余3条评论
4个回答

5
好的,让我们考虑为什么finalize和Co存在问题:正如您所知,不能保证在VM关闭之前finalize将被调用,这意味着特殊的清理代码不一定会运行(我认为这是个糟糕的决定,我不认为在清理时运行finalize队列会有任何问题,但这就是事实)。同样,C#中也存在完全相同的情况。
现在您的对象只消耗内存,在VM销毁时,操作系统会自动清理内存,因此finalize存在问题的唯一情况对您来说并不重要。因此,您确实可以使用此变体,并且它将完美地工作,但可能不被认为是一个伟大的架构设计——一旦您将资源添加到未由操作系统正确处理清理的C++代码中,您将遇到问题。
另请注意,实现finalizer会导致GC产生额外的开销,并且意味着需要两个周期来清理其中一个对象(无论您做什么,请不要在finalize方法中保存对象)。

1
说得对。但是由于GC不受外部“压力”的影响,它可能不会像它本来可以的那样积极地释放对象(忽略最终器完全不被调用的事实)——当外部资源添加到混合中时,这可能导致“资源不足”的情况。一个例子是使用直接分配缓冲区的ByteBuffer。 - user166390
谢谢Voo。我的问题在于我没有完全理解“覆盖finalize方法是不好的”规则背后的确切原因。Ratchet Freak和Konstantin Komissarchik基本上给出了相同的答案,但由于你的回答是第一个并且更加详细,所以我接受了它。 - Alex Che
@pst,我创建了一个相关的问题:https://dev59.com/ZFnUa4cB1Zd3GeqPZE18。你能否协作一下? - Alex Che

2

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

对于您的情况,使用finalize是完全可以的。


1

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

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


0

那么我们如何使用幻影引用来实现呢?

  1. 为您的本地 intPtr 对象创建一个包装器对象。在包装器对象上创建一个带有引用队列的幻影引用。
  2. 创建并维护一个幻影引用到 intPtr 的映射。
  3. 创建一个线程,该线程将监视引用队列以获取已完成的包装器对象实例。
  4. 此线程将从引用队列中获取幻影引用,使用幻影引用查找 intPtr,并调用 native int 对象的析构函数。
  5. 在所有这些操作进行时,您可以愉快地在 Java 代码中使用包装器对象。

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