【发布时间】:2017-10-21 01:42:48
【问题描述】:
根据我作为 C++/Java/Android 开发人员的经验,我了解到终结器几乎总是一个坏主意,唯一的例外是管理 java 调用 C 所需的“本机对等”对象/C++ 代码通过 JNI。
我知道JNI: Properly manage the lifetime of a java object question,但这个问题解决了无论如何都不使用终结器的原因,对于本地同行也不使用。因此,这是对上述问题中答案的反驳的问题/讨论。
Joshua Bloch 在他的 Effective Java 中明确将此案例列为他关于不使用终结器的著名建议的例外:
终结器的第二个合法用途涉及具有本地对等点的对象。本机对等点是普通对象通过本机方法委托给其的本机对象。因为本地对等点不是普通对象,所以垃圾收集器不知道它,也无法在其 Java 对等点被回收时回收它。假设本地对等点没有关键资源,终结器是执行此任务的合适工具。如果本地对等点拥有必须立即终止的资源,则该类应具有显式终止方法,如上所述。终止方法应该执行释放关键资源所需的任何操作。终止方法可以是本机方法,也可以调用一个。
(另见"Why is the finalized method included in Java?"stackexchange 上的问题)
然后我在 Google I/O '17 上观看了真正有趣的 How to manage native memory in Android 演讲,Hans Boehm 实际上主张反对使用终结器来管理 Java 对象的本地对等点,还引用了 Effective Java作为参考。在快速提到为什么显式删除本地对等点或基于范围自动关闭可能不是一个可行的选择后,他建议改用java.lang.ref.PhantomReference。
他提出了一些有趣的观点,但我并不完全相信。我将尝试浏览其中的一些并陈述我的疑问,希望有人能进一步阐明它们。
从这个例子开始:
class BinaryPoly {
long mNativeHandle; // holds a c++ raw pointer
private BinaryPoly(long nativeHandle) {
mNativeHandle = nativeHandle;
}
private static native long nativeMultiply(long xCppPtr, long yCppPtr);
BinaryPoly multiply(BinaryPoly other) {
return new BinaryPoly ( nativeMultiply(mNativeHandle, other.mNativeHandler) );
}
// …
static native void nativeDelete (long cppPtr);
protected void finalize() {
nativeDelete(mNativeHandle);
}
}
如果 java 类持有对在终结器方法中删除的本机对等点的引用,Bloch 列出了这种方法的缺点。
终结器可以按任意顺序运行
如果两个对象变得不可达,终结器实际上以任意顺序运行,这包括两个指向彼此的对象同时变得不可达的情况,它们可能以错误的顺序被终结,这意味着第二个对象be finalized 实际上会尝试访问已经完成的对象。 [...] 因此,您可以获得悬空指针并查看已释放的 c++ 对象 [...]
举个例子:
class SomeClass {
BinaryPoly mMyBinaryPoly:
…
// DEFINITELY DON’T DO THIS WITH CURRENT BinaryPoly!
protected void finalize() {
Log.v(“BPC”, “Dropped + … + myBinaryPoly.toString());
}
}
好的,但是如果 myBinaryPoly 是一个纯 Java 对象,这难道不是真的吗?据我了解,问题来自在其所有者的终结器中对其可能已终结的对象进行操作。如果我们只使用一个对象的终结器来删除它自己的私有本地对等点而不做任何其他事情,我们应该没问题,对吧?
可以在本地方法运行期间调用终结器
按 Java 规则,但目前不在 Android 上:
对象 x 的终结器可能在 x 的方法之一仍在运行并访问本机对象时被调用。
multiply() 编译成的伪代码用于解释这一点:
BinaryPoly multiply(BinaryPoly other) {
long tmpx = this.mNativeHandle; // last use of “this”
long tmpy = other.mNativeHandle; // last use of other
BinaryPoly result = new BinaryPoly();
// GC happens here. “this” and “other” can be reclaimed and finalized.
// tmpx and tmpy are still needed. But finalizer can delete tmpx and tmpy here!
result.mNativeHandle = nativeMultiply(tmpx, tmpy)
return result;
}
这太可怕了,我真的松了一口气,这不会发生在 android 上,因为我的理解是 this 和 other 在超出范围之前会收集垃圾!考虑到this 是调用该方法的对象,而other 是该方法的参数,这更加奇怪,因此它们都应该已经在调用该方法的范围内“处于活动状态”。
对此的快速解决方法是在 this 和 other 上调用一些虚拟方法(丑陋!),或者将它们传递给本机方法(然后我们可以在其中检索 mNativeHandle 并对其进行操作)。等等……this 已经默认是原生方法的参数之一!
JNIEXPORT void JNICALL Java_package_BinaryPoly_multiply
(JNIEnv* env, jobject thiz, jlong xPtr, jlong yPtr) {}
this 怎么可能被垃圾回收?
终结器可能被推迟太久
“为了使其正常工作,如果您运行的应用程序分配了大量本机内存和相对较少的 Java 内存,那么垃圾收集器实际上可能不会足够迅速地运行以实际调用终结器 [...] 所以您实际上可能不得不偶尔调用 System.gc() 和 System.runFinalization(),这很棘手 [...]”
如果本地对等点只能被它所绑定的单个 java 对象看到,这对系统的其余部分不是透明的,因此 GC 应该只需要管理 Java 对象的生命周期这是一个纯Java的吗?很明显,我在这里看不到一些东西。
终结器实际上可以延长 java 对象的生命周期
[...] 有时终结器实际上将 java 对象的生命周期延长到另一个垃圾收集周期,这意味着对于分代垃圾收集器来说,它们实际上可能会使其存活到老年代,并且生命周期可能会大大延长只是有一个终结器的结果。
我承认我真的不明白这里的问题是什么以及它与拥有本地同行有何关系,我会进行一些研究并可能更新问题:)
总结
目前,我仍然认为使用某种 RAII 方法是在 java 对象的构造函数中创建本地对等点并在 finalize 方法中删除实际上并不危险,前提是:
- 本机对等点不持有任何关键资源(在这种情况下,应该有一个单独的方法来释放资源,本机对等点只能充当本机领域中的 java 对象“对应物”)
- 本机对等点不跨线程或在其析构函数中执行奇怪的并发操作(谁愿意这样做?!?)
- 本机对等指针永远不会在 java 对象之外共享,只属于单个实例,并且只能在 java 对象的方法内部访问。在 Android 上,java 对象可以访问同一类的另一个实例的本地对等点,就在调用接受不同本地对等点的 jni 方法之前,或者更好的是,只是将 java 对象传递给本地方法本身
- java 对象的终结器只删除它自己的本地对等点,其他什么都不做
是否应该添加任何其他限制,或者即使遵守所有限制,也确实无法确保终结器是安全的?
【问题讨论】:
-
通常,如果管理代码未能执行此操作,并且通常会打印日志警告,则终结器被视为调用
dispose()(或其他)的备用选项。 -
请在为重复投票之前阅读这两个问题。
-
我相信这不应该被标记为重复。最初的问题是“我应该如何自动删除本机对等对象?”答案是“使用终结器”。我的问题特别是关于建议不要使用终结器的演讲(甚至与原始问题中接受的答案相矛盾!),我正在寻求更多见解和其他知情意见以了解原因这样的建议。
-
感谢您开始本次讨论。不幸的是,这样的公开讨论不太适合 SO 的格式,但我认为这是这个话题的最佳场所。首先,在我的书中,
private BinaryPoly(long nativeHandle)不应该存在,整个例子都是非常人为的。 -
这在可预见的未来(如果有的话)可能不会涉及 Android,但请注意 Object.finalize() 在 Java 9 中是 deprecated,尽管注释中没有
forRemoval=true属性,所以在这个阶段没有明确的计划来实际删除它。它并没有消除支持使用finalize()的论点的任何优点,但我认为它值得一提(毕竟问题确实有java标签)。
标签: java android java-native-interface finalize finalization