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

Java 为什么我不应该在JNI中重用jclass和/或jmethodID?,java,java-native-interface,Java,Java Native Interface,这是一个与相关的问题,但这篇文章已经解决了,现在我想改变问题的方向 在使用JNI时,有必要询问jclass的JNIEnv对象,以及将在C/C++代码中使用的每个类和方法的jmethodID。为了清楚起见,我想从C/C++调用Java构造函数或方法 由于从Java到C/C++(和viceversa)的通信成本很高,我最初认为将其最小化的一种方法是重用jclass和jmethodID。因此,我将此实例保存在全局变量中,如下所示: jclass someClass = NULL; jmetho

这是一个与相关的问题,但这篇文章已经解决了,现在我想改变问题的方向

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

由于从Java到C/C++(和viceversa)的通信成本很高,我最初认为将其最小化的一种方法是重用
jclass
jmethodID
。因此,我将此实例保存在全局变量中,如下所示:

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函数重用
    jclass
    jmethodID
    是非法的?我认为这些价值观总是一样的
  • 出于好奇:为每个JNI函数初始化所有必要的
    jclass
    jmethodID
    有什么影响/开销

我记得jclass是调用方法的本地类,因此不能缓存,但是方法id可以是。有关更多详细信息,请参阅


抱歉,我不知道性能方面的问题,任何时候我使用JNI与手头的任务相比都是微不足道的。

这里的规则很清楚。方法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);
check宏调用:

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();
    }
}

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


然后,在
JNI\u onload
中,您可以对它们调用
DeleteGlobalRef

您可以缓存并使用您的方法/函数/类ID,如果您为其编写了适当的代码,则可以安全地使用它们。我已经回答并发布了关于如何遵循IBM缓存性能建议的明确代码。

正如其他人所写

< L>可以在一个静态C++变量中存储<代码> JMeodod> /Cube,没有问题< /LI> <> LI>通过调用<代码> EnV--NexGloBrdFor(< /Calp> < /LI>),将C++中的本地代码<代码> JOBJET< <代码>或<代码> JCys<代码>转换为全局对象。 我只想在此添加一个附加信息: 将JC类存储在静态C++变量中的主要原因是,您认为每次调用“代码> EnV--FunCype())/>代码是一个性能问题。 但是我在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事件调度程序线程。如果我在本机Windows线程中执行与我创建的
CreateThread()
相同的代码,那么它的运行速度将提高10倍。为什么?

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


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

  • 全局
    jobject
    jclass
    在任何Java线程中都有效
  • 本地对象仅在调用线程的
    JNIEnv
    中的一个函数调用中有效,并且在JNI代码返回Java时被垃圾收集
  • 现在取决于你使用的线程:如果你登记C++函数,使用<代码> Env> > RealStnjava()/代码>,Java代码正在调用你的JNI函数,那么你必须把所有的对象,你想以后使用,作为全局对象,否则它们会被垃圾收集。 但是,如果您使用

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

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


    开发jnic
    java -Xcheck:jni -jar MyApplication.jar
    
    FATAL ERROR in native method: Bad global or local ref passed to JNI
    
    #
    # A fatal error has been detected by the Java Runtime Environment:
    #
    #  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x6e8655d5, pid=4692, tid=4428
    #
    etc...