【问题标题】:Why I should not reuse a jclass and/or jmethodID in JNI?为什么我不应该在 JNI 中重用 jclass 和/或 jmethodID?
【发布时间】:2019-08-24 20:55:44
【问题描述】:

这是一个与a previous post相关的问题,但是这个帖子已经解决了,现在我想改变问题的方向。

在使用 JNI 时,必须向 JNIEnv 对象询问 jclassjmethodID,以获取将在 C/C++ 代码中使用的每个类和方法。为了清楚起见,我想从 C/C++ 调用 Java 构造函数或方法。

由于从 Java 到 C/C++(反之亦然)的通信成本很高,我最初认为减少这种情况的一种方法是重用 jclassjmethodID。因此,我将此实例保存在全局变量中,如下所示:

jclass    someClass  = NULL;
jmethodID someMethod = NULL;

JNIEXPORT jobject JNICALL Java_example_method1(JNIEnv *env, jobject jobj) {
    // initialize someClass and someMethod if they are NULL
    // use someClass and someMethod to call java (for example, thru NewObject)
}

JNIEXPORT jobject JNICALL Java_example_method2(JNIEnv *env, jobject jobj) {
    // initialize someClass and someMethod if they are NULL
    // use someClass and someMethod to call java again
}

一个更具体(且有用)的示例,我用它来从我的 JNI 函数的任何地方抛出异常:

jclass    jniExceptionClass           = NULL;

void throwJavaException(JNIEnv *env, const char* msg) {
    if (!jniExceptionClass) {
        jniExceptionClass = env->FindClass("example/JNIRuntimeException");
    }
    if (jniExceptionClass)
        env->ThrowNew(jniExceptionClass, msg);
    }
}

问题是我继续使用这个模式,得到了一个分段错误,只能通过不重用这个变量来解决(这是上一篇文章的解决方案)。

问题是:

  • 为什么通过不同的 JNI 函数重用 jclassjmethodID 是非法的?我认为这些值始终相同。
  • 只是为了好奇:为每个 JNI 函数初始化所有必要的 jclassjmethodID 的影响/开销是什么?

【问题讨论】:

    标签: java java-native-interface


    【解决方案1】:

    这里的规则很明确。方法 ID 和字段 ID 值是永久的。你可以挂在他们身上。查找需要一些时间。

    另一方面,

    jclass 通常是本地引用。本地引用最多在对 JNI 函数的单个调用的持续时间内存在。

    如果你需要优化,你必须要求JVM为你做一个全局引用。获取和保留对java.lang.String 等公共类的引用并不少见。

    当然,持有对类的此类引用会阻止它(该类)被垃圾回收。

    jclass local = env->FindClass(CLS_JAVA_LANG_STRING);
    _CHECK_JAVA_EXCEPTION(env);
    java_lang_string_class = (jclass)env->NewGlobalRef(local);
    _CHECK_JAVA_EXCEPTION(env);
    env->DeleteLocalRef(local);
    _CHECK_JAVA_EXCEPTION(env);
    

    检查宏调用:

    static inline void
    check_java_exception(JNIEnv *env, int line)
    {
        UNUSED(line);
        if(env->ExceptionOccurred()) {
    #ifdef DEBUG
            fprintf(stderr, "Java exception at rlpjni.cpp line %d\n", line);
            env->ExceptionDescribe();
        abort();
    #endif
            throw bt_rlpjni_java_is_upset();
        }
    }
    

    【讨论】:

    【解决方案2】:

    正如其他人已经写的那样

    1. 您可以毫无问题地将jmethodID 存储在静态 C++ 变量中
    2. 通过调用env->NewGloablRef() 将它们转换为全局对象后,您可以将本地jobjectjclass 存储在静态C++ 变量中

    我只想在此处添加其他信息: 将 jclass 存储在静态 C++ 变量中的主要原因是您认为每次调用 env->FindClass() 是一个性能问题。

    但我使用 Windows 上的 QueryPerformanceCounter() API 的性能计数器测量了 FindClass() 的速度。结果令人惊讶:

    在我的具有 3,6 GHz CPU 的计算机上执行

    jcass p_Container = env->FindClass("java/awt/Container");
    

    需要 0.01 毫秒到 0.02 毫秒。这是令人难以置信的快。我查看了 Java 源代码,他们使用存储类的字典。这似乎非常有效地实现。

    我又测试了一些类,结果如下:

    Elapsed 0.002061 ms for java/net/URL
    Elapsed 0.044390 ms for java/lang/Boolean
    Elapsed 0.019235 ms for java/lang/Character
    Elapsed 0.018372 ms for java/lang/Number
    Elapsed 0.017931 ms for java/lang/Byte
    Elapsed 0.017589 ms for java/lang/Short
    Elapsed 0.017371 ms for java/lang/Integer
    Elapsed 0.015637 ms for java/lang/Double
    Elapsed 0.018173 ms for java/lang/String
    Elapsed 0.015895 ms for java/math/BigDecimal
    Elapsed 0.016204 ms for java/awt/Rectangle
    Elapsed 0.016272 ms for java/awt/Point
    Elapsed 0.001817 ms for java/lang/Object
    Elapsed 0.016057 ms for java/lang/Class
    Elapsed 0.016829 ms for java/net/URLClassLoader
    Elapsed 0.017807 ms for java/lang/reflect/Field
    Elapsed 0.016658 ms for java/util/Locale
    Elapsed 0.015720 ms for java/lang/System
    Elapsed 0.014669 ms for javax/swing/JTable
    Elapsed 0.017276 ms for javax/swing/JComboBox
    Elapsed 0.014777 ms for javax/swing/JList
    Elapsed 0.015597 ms for java/awt/Component
    Elapsed 0.015223 ms for javax/swing/JComponent
    Elapsed 0.017385 ms for java/lang/Throwable
    Elapsed 0.015089 ms for java/lang/StackTraceElement
    

    以上值来自 Java 事件调度线程。如果我在 CreateThread() 创建的本地 Windows 线程中执行相同的代码,它的运行速度甚至会快 10 倍。为什么?

    因此,如果您不经常调用 FindClass(),那么在调用 JNI 函数时按需调用它绝对没有问题,而不是创建全局引用并将其存储在静态变量中。


    另一个重要的话题是线程安全。在 Java 中,每个线程都有自己独立的 JNIEnv 结构。

    1. 全局jobjectjclass 在任何Java 线程中都有效。
    2. 本地对象仅在调用线程的JNIEnv 中的一个函数调用中有效,并在 JNI 代码返回 Java 时进行垃圾回收。

    现在这取决于您使用的线程:如果您使用 env->RegisterNatives() 注册您的 C++ 函数并且 Java 代码正在调用您的 JNI 函数,那么您必须将您以后想要使用的所有对象存储为全局对象否则他们会被垃圾回收。

    但是如果您使用CraeteThread() API(在Windows 上)创建自己的线程并通过调用AttachCurrentThreadAsDaemon() 获得JNIEnv 结构,那么将完全适用其他规则:因为它是您自己的线程,所以永远不会返回控制权对于 Java,垃圾收集器永远不会清理您在线程上创建的对象,您甚至可以毫无问题地将本地对象存储在静态 C++ 变量中(但不能从其他线程访问这些对象)。在这种情况下,使用env->DeleteLocalRef() 手动清理所有本地实例非常重要,否则会出现内存泄漏。 (但是,当您调用 DetachCurrentThread() 时,垃圾收集器可能会释放所有本地对象,我从未测试过。)

    我强烈建议将所有本地对象加载到一个包装类中,该类在其析构函数中调用DeleteLocalRef()。这是避免内存泄漏的万无一失的方法。


    开发 JNI 代码可能非常繁琐,并且您可能会遇到不理解的崩溃。要查找崩溃的原因,请打开 DOS 窗口并使用以下命令启动 Java 应用程序:

    java -Xcheck:jni -jar MyApplication.jar
    

    然后你会看到JNI代码中发生了什么问题。例如:

    FATAL ERROR in native method: Bad global or local ref passed to JNI
    

    您将在 Java 创建的日志文件中找到堆栈跟踪,该文件位于您拥有 JAR 文件的同一文件夹中:

    #
    # A fatal error has been detected by the Java Runtime Environment:
    #
    #  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x6e8655d5, pid=4692, tid=4428
    #
    etc...
    

    【讨论】:

    • 当然,只要他们设法完成我的注册...
    【解决方案3】:

    JNI_OnLoad内部,需要在缓存FindClass返回的jclass值上使用NewGlobalRef

    然后,在JNI_OnUnload 中,您对它们调用DeleteGlobalRef

    【讨论】:

    • JNI_OnLoad 和 JNI_OnUnload 不能成对调用,Unload 完全取决于 GC 是否收集了类加载器。没有参考我要说的内容,但从逻辑上讲,当所有全局引用都被 GC 时,GC 会理想地收集类加载器,因此在这种情况下可能存在内存泄漏。
    【解决方案4】:

    我记得 jclass 对于调用方法来说是本地的,所以不能被缓存,但是方法 id 可以。请参阅http://java.sun.com/javase/6/docs/technotes/guides/jni/spec/design.html 了解更多详细信息。

    对不起,我不知道性能方面,任何时候我使用过 JNI,与手头的任务相比,它都是微不足道的。

    【讨论】:

      【解决方案5】:

      您可以缓存和使用您的方法/函数/类 ID,并在适当编码的情况下安全地使用它们。我已回答 a similar question here on SO 并发布了有关如何遵循 IBM 缓存性能建议的明确代码。

      【讨论】:

        猜你喜欢
        • 2014-09-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-10-09
        • 2015-12-09
        • 1970-01-01
        相关资源
        最近更新 更多