java - 对于 native 对等对象生命周期管理,是否真的应该避免 Java 终结器?

标签 java android java-native-interface googleio

根据我作为 C++/Java/Android 开发人员的经验,我了解到终结器几乎总是一个坏主意,唯一的异常(exception)是管理 Java 调用 C/C++ 代码所需的“本地对等”对象通过 JNI。
我知道 JNI: Properly manage the lifetime of a java object question ,但这个问题解决了原因 无论如何都不要使用终结器,也不要用于 native 同行 .因此,这是对上述问题中答案的反驳的问题/讨论。
约书亚·布洛赫 (Joshua Bloch) 在他的 Effective Java 中明确地将此案例列为他关于不使用终结器的著名建议的异常(exception):

A second legitimate use of finalizers concerns objects with native peers. A native peer is a native object to which a normal object delegates via native methods. Because a native peer is not a normal object, the garbage collector doesn't know about it and can’t reclaim it when its Java peer is reclaimed. A finalizer is an appropriate vehicle for performing this task, assuming the native peer holds no critical resources. If the native peer holds resources that must be terminated promptly, the class should have an explicit termination method, as described above. The termination method should do whatever is required to free the critical resource. The termination method can be a native method, or it can invoke one.


(另见 "Why is the finalized method included in Java?" 关于 stackexchange 的问题)
然后我看了很有趣的How to manage native memory in Android在 Google I/O '17 上的演讲,Hans Boehm 实际上在那里提倡 反对使用终结器来管理 Java 对象的 native 对等方 ,还引用了 Effective Java 作为引用。在快速提到为什么显式删除 native 对等点或基于范围自动关闭可能不是可行的替代方案之后,他建议使用 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 类保存对在终结器方法中被删除的 native 对等点的引用的情况下,Bloch 列出了这种方法的缺点。
终结器可以以任意顺序运行

If two objects become unreachable, the finalizers actually run in arbitrary order, that includes the case when two objects who point to each others become unreachable at the same time they can be finalized in the wrong order, meaning that the second one to be finalized actually tries to access an object that’s already been finalized. [...] As a result of that you can get dangling pointers and see deallocated c++ objects [...]


作为一个例子:
class SomeClass {
    BinaryPoly mMyBinaryPoly:
    …
    // DEFINITELY DON’T DO THIS WITH CURRENT BinaryPoly!
    protected void finalize() {
        Log.v(“BPC”, “Dropped + … + myBinaryPoly.toString());   
    }
}
好的,但是如果 myBinaryPoly 是一个纯 Java 对象,这不是真的吗?据我了解,问题来自在其所有者的终结器中操作可能已终结的对象。如果我们只使用对象的终结器来删除它自己的私有(private)本地对等点而不做其他任何事情,我们应该没问题,对吧?
可以在本地方法运行时调用终结器

By Java rules, but not currently on Android:
Object x’s finalizer may be invoked while one of x’s methods is still running, and accessing the native object.


什么的伪代码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 上,因为我理解的是 thisother在它们超出范围之前收集垃圾!考虑到 this,这就更奇怪了。是调用该方法的对象,即 other是方法的参数,因此它们都应该已经在调用方法的范围内“处于 Activity 状态”。
对此的快速解决方法是在 this 上调用一些虚拟方法。和 other (丑!),或者将它们传递给本地方法(然后我们可以在其中检索 mNativeHandle 并对其进行操作)。等等... this默认情况下已经是 native 方法的参数之一!
JNIEXPORT void JNICALL Java_package_BinaryPoly_multiply
(JNIEnv* env, jobject thiz, jlong xPtr, jlong yPtr) {}
怎么可以this可能被垃圾收集?
终结器可能会延迟太久

“For this to work correctly, if you run an application that allocates lots of native memory and relatively little java memory it may actually not be the case that the garbage collector runs promptly enough to actually invoke finalizers [...] so you actually may have to invoke System.gc() and System.runFinalization() occasionally, which is tricky to do [...]”


如果 native 对等方仅被它所绑定(bind)的单个 Java 对象看到,那么这个事实对系统的其余部分是不是透明的,因此 GC 应该只需要管理 Java 对象的生命周期,因为它是一个纯java一个?很明显,我在这里看不到一些东西。
终结器实际上可以延长 java 对象的生命周期

[...] Sometimes finalizers actually extend the lifetime of the java object for another garbage collection cycle, which means for generational garbage collectors they may actually cause it to survive into the old generation and the lifetime may be greatly extended as a result of just having a finalizer.


我承认我真的不明白这里的问题是什么以及它与本地同行有什么关系,我会做一些研究并可能更新问题:)
总结
现在,我仍然相信使用一种 RAII 方法是在 java 对象的构造函数中创建 native 对等点并在 finalize 方法中删除实际上并不危险,前提是:
  • native 对等方不持有任何关键资源(在这种情况下,应该有一个单独的方法来释放资源, native 对等方必须仅充当 native 领域中的 java 对象“对应物”)
  • native 对等方不会跨线程或在其析构函数中执行奇怪的并发操作(谁会想要这样做?!?)
  • native 对等指针永远不会在 java 对象外部共享,只属于单个实例,并且只能在 java 对象的方法内部访问。在 Android 上,一个 java 对象可以访问同一个类的另一个实例的本地 peer,就在调用接受不同本地 peer 的 jni 方法之前,或者更好的是,只需将 java 对象传递给本地方法本身
  • java 对象的终结器只删除它自己的本地对等体,不做其他任何事情

  • 是否应该添加任何其他限制,或者即使遵守所有限制,也确实没有办法确保终结器是安全的?

    最佳答案

    finalize和其他使用对象生命周期知识的方法有一些细微差别:

  • 知名度 :您是否保证对象 o 的所有写入方法对终结器都是可见的(即,对象 o 上的最后一个操作与执行终结的代码之间存在发生之前的关系)?
  • 可达性 :您如何保证对象 o 不会过早销毁(例如,当它的一个方法正在运行时),这是 JLS 所允许的?确实如此 happen并导致崩溃。
  • 订购 : 你能强制执行某个对象最终确定的顺序吗?
  • 终止 :当您的应用程序终止时,您是否需要销毁所有对象?
  • 吞吐量 :与确定性方法相比,基于 GC 的方法提供的释放吞吐量要小得多。

  • 使用终结器可以解决所有这些问题,但它需要相当数量的代码。汉斯-J。博姆有 a great presentation它显示了这些问题和可能的解决方案。
    为了保证可见性,您必须同步代码,即在常规方法中放置具有 Release 语义的操作,并在终结器中放置具有 Acquire 语义的操作。例如:
  • 位于 volatile 的一家商店在每个方法结束时 + 阅读相同 volatile在终结器中。
  • 在每个方法结束时释放对对象的锁定 + 在终结器开始时获取锁(参见 Boehm 幻灯片中的 keepAlive 实现)。

  • 为了保证可达性(当语言规范尚未保证时),您可以使用:
  • 上述同步方法也确保了可达性。
  • 将对必须保持可达的对象(= non-finalizable)的引用作为参数传递给 native 方法。在 the talk you reference , nativeMultiplystatic ,因此 this可能会被垃圾收集。
  • Reference#reachabilityFence 来自 Java 9+。

  • 素色的区别finalizePhantomReferences是后者让您可以更好地控制最终确定的各个方面:
  • 可以有多个队列接收幻像引用 为每个线程选择一个执行终结的线程。
  • 可以在进行分配的同一线程中完成(例如,线程本地 ReferenceQueues)。
  • 更容易执行排序:保持对对象的强引用 B必须在 A 时保持 Activity 状态最终确定为 PhantomReference 的字段至 A ;
  • 更容易实现安全终止,因为您必须保留 PhantomRefereces强可达,直到它们被 GC 入队。
  • 关于java - 对于 native 对等对象生命周期管理,是否真的应该避免 Java 终结器?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44095247/

    相关文章:

    java - keyReleases 在 Linux 中模拟 keyPresses (java Swing GUI)

    android - 录制在 android 中安装/运行的任何应用程序的视频

    java - matlab 调用 Findclass JNI 崩溃

    android - 使用 NDK 在 Android Studio 1.0.2 中生成 .so 文件

    java - servlet 发生 mySql 列未找到错误

    java - Hibernate 使用 Equals 生成 POJO

    android - 水平滚动日历 View

    android - 我可以使 ListView 项目不可选吗?

    java - 更新 JNI 中的 jint 变量

    java - "Assigned value is never used"警告是什么意思?