我知道JNI:正确管理java对象生命周期问题,但是这个问题解决的是不使用终结器的原因,即使是对于本地对等体。所以这是一个关于上述问题答案的质疑和讨论。
在他的Effective Java中,Joshua Bloch明确将这种情况列为他著名建议的例外,即不使用终结器。
一个合法的使用finalizers的例子与具有本地对等体的对象相关。本地对等体是一个通过本地方法委托给普通对象的本地对象。因为本地对等体不是普通对象,垃圾回收器不知道它,并且不能在其Java对等体被回收时回收它。假设本地对等体不持有关键资源,那么finalizer是执行此任务的适当工具。如果本地对等体持有必须立即终止的资源,则该类应具有显式终止方法,如上所述。终止方法应执行释放关键资源所需的任何操作。终止方法可以是本地方法,也可以调用本地方法。(另请参见stackexchange上的"为什么Java中包含了finalized方法?"问题。)
我看了 Google I/O '17 上 Hans Boehm 的 如何在 Android 中管理本地内存的演讲,发现他实际上反对使用 finalizer 来处理 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类持有对本地对等体的引用,并在finalizer方法中删除该引用时,Bloch列出了这种方法的缺点。
Finalizers可以以任意顺序运行
如果两个对象变得不可达,那么finalizers实际上会以任意顺序运行,包括指向彼此的两个对象在同时变得不可达的情况下,它们可能以错误的顺序被终结,这意味着最后被终结的对象实际上尝试访问已经被终结的对象。[...] 由此可能导致悬空指针并看到已释放的C ++对象[...]
作为示例:
class SomeClass {
BinaryPoly mMyBinaryPoly:
…
// DEFINITELY DON’T DO THIS WITH CURRENT BinaryPoly!
protected void finalize() {
Log.v(“BPC”, “Dropped + … + myBinaryPoly.toString());
}
}
好的,但是如果myBinaryPoly是一个纯Java对象,这也是真的吗?据我所知,问题出在处理可能已经完成的对象的所有者的finalizer内部。假设我们只使用对象的finalizer来删除其自己的私有本地对象,并且没有做任何其他事情,那么我们应该没问题,对吧?
Finalizer 可能在本地方法运行时被调用
按照 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;
}
这很可怕,我很庆幸这种情况不会发生在安卓上,因为我的理解是
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对象的终结器仅删除自己的本地对等体,不执行其他操作
是否还应添加其他限制,或者即使遵守所有限制,也无法确保终结器安全?
forRemoval=true
属性,因此目前还没有明确的计划实际删除它。这并不会削弱支持使用finalize()
的论点的任何好处,但我认为这值得一提(毕竟该问题具有java
标签)。 - Hugues M.