为什么我不应该在JNI中重用jclass和/或jmethodID?

时间:2010-01-19 11:36:33

标签: java java-native-interface

这是一个与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会产生什么影响/开销?

5 个答案:

答案 0 :(得分:57)

这里的规则很清楚。方法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();
    }
}

答案 1 :(得分:7)

JNI_OnLoad内,您需要在缓存NewGlobalRef之前对jclass返回的FindClass值使用JNI_OnUnload

然后,在DeleteGlobalRef内,您就可以调用{{1}}。

答案 2 :(得分:4)

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

对不起,我不知道性能方面,只要我使用JNI,它与手头的任务相比毫无意义。

答案 3 :(得分:4)

正如其他人已写的那样

  1. 您可以将jmethodID存储在静态C ++变量中而不会出现问题
  2. 通过调用jobject
  3. 将本地jclassenv->NewGloablRef()转换为全局对象后,可以将其存储在静态C ++变量中

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

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

    在具有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时被垃圾收集。
    3. 现在它取决于您正在使用的线程:如果您使用env->RegisterNatives()注册C ++函数并且Java代码正在调用JNI函数,那么您必须存储以后要使用的所有对象,全局对象,否则他们将收集垃圾。

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

      我强烈建议将所有本地对象加载到一个包装类中,该类在其析构函数中调用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...
      

答案 4 :(得分:0)

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