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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2093112/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 03:27:36  来源:igfitidea点击:

Why I should not reuse a jclass and/or jmethodID in JNI?

javajava-native-interface

提问by YuppieNetworking

This is a question related to a previous post, but this post was solved and now I wanted to change the direction of the question.

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

When working with JNI, it is necessary to ask the JNIEnvobject for jclassand jmethodIDfor each class and method that will be used in the C/C++ code. Just to be clear, I want to call Java contructors or methods from C/C++.

当与工作JNI,有必要询问JNIEnv对象jclassjmethodID用于将在C / C ++代码中使用每个类和方法。为了清楚起见,我想从 C/C++ 调用 Java 构造函数或方法。

Since the communication from Java to C/C++ (and viceversa) is costly, I initially thought that one way to minimize this was to reuse the jclassand jmethodID. Therefore, I saved this instances in global variables as follows:

由于从 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
}

A more specific (and useful) example, which I use to throw exceptions from anywhere in my JNI functions:

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

jclass    jniExceptionClass           = NULL;

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

The problem is that I continued to use this patternand got a segmentation fault that was only solved by not-reusing this variables (this was the solution to the previous post).

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

The questions are:

问题是:

  • Why is it illegal to reuse the jclassand jmethodIDthru different JNI functions? I thought that this values were always the same.
  • Just for curiosity: what is the impact/overhead of initializing all necessary jclassand jmethodIDfor each JNI function?
  • 为什么重用jclassjmethodID通过不同的 JNI 函数是非法的?我认为这个值总是相同的。
  • 只是出于好奇:初始化所有必要的jclassjmethodID每个 JNI 函数的影响/开销是什么?

采纳答案by bmargulies

The rules here are clear. Method ID and field ID values are forever. You can hang onto them. The lookups take some time.

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

jclass, on the other hand, is generally a local reference. A local reference survives, at most, the duration of a single call to a JNI function.

jclass另一方面,通常是本地参考。本地引用最多在对 JNI 函数的单次调用期间存活。

If you need to optimize, you have to ask the JVM to make a global reference for you. It's not uncommon to acquire and keep references to common classes like java.lang.String.

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

Holding such a reference to a class will prevent it (the class) from being garbage-collected, of course.

当然,持有这样一个对类的引用将防止它(类)被垃圾收集。

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

The check macro calls:

检查宏调用:

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

回答by vickirk

As I remember jclass is will be local to the calling method so can't be cached, however the method id can be. See http://java.sun.com/javase/6/docs/technotes/guides/jni/spec/design.htmlfor more detaild info.

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

Sorry, I don't know about the performance aspect, any time I've used JNI it has been insignificant compared to the task in hand.

抱歉,我不知道性能方面,每次使用 JNI 与手头的任务相比都微不足道。

回答by Gregory Pakosz

Inside JNI_OnLoad, you need to use NewGlobalRefon the jclassvalues returned by FindClassbefore caching them.

在内部JNI_OnLoad,您需要在缓存它们之前使用返回NewGlobalRefjclassFindClass

Then, inside JNI_OnUnloadyou call DeleteGlobalRefon them.

然后,JNI_OnUnloadDeleteGlobalRef在里面呼唤他们。

回答by JoshDM

You can cache and use your method/function/class IDs and safely use them if you code for it appropriately. I have answered a similar question here on SOand posted explicit code for how to follow IBM's caching performance recommendations.

您可以缓存和使用您的方法/函数/类 ID,并且如果您对其进行适当的编码,则可以安全地使用它们。我已经在 SO 上回答了一个类似的问题,并发布了关于如何遵循 IBM 的缓存性能建议的显式代码。

回答by Elmue

As others already wrote

正如其他人已经写的那样

  1. You can store jmethodIDin a static C++ variable without problems
  2. You can store local jobjector jclassin a static C++ variable after converting them to global objects by calling env->NewGloablRef()
  1. 您可以jmethodID毫无问题地存储在静态 C++ 变量中
  2. 通过调用将它们转换为全局对象后,您可以存储本地jobjectjclass静态 C++ 变量env->NewGloablRef()

I just want to add an additional information here: The major reason for storing jclass in a static C++ variable will be that you think it is a performance issue to call env->FindClass()each time.

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

But I measured the speedof FindClass()with a performance counter with the QueryPerformanceCounter()API on Windows. And the result was astonishing:

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

On my computer with a 3,6 GHz CPU the execution of

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

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

takes between 0,01 ms and 0,02 ms. That is incredibly fast. I looked into the Java source code and they use a Dictionary where the classes are stored. This seems to be implemented very efficiently.

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

I tested some more classes and here is the result:

我测试了更多类,结果如下:

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

The above values are from the Java event dispatcher thread. If I execute the same code in a native Windows thread that has been created by CreateThread()by me, it runs even 10 times faster. Why?

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

So if you do not call FindClass()very frequently there is absolutely no problem calling it on demand when your JNI function is called instead of creating a global reference and storing it in a static variable.

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



Another important topic is thread safety. In Java each thread has it's own independent JNIEnvstructure.

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

  1. Global jobjector jclassare valid in any Java thread.
  2. Local objects are only valid in one function call in the JNIEnvof the calling thread and are garbage collected when JNI code returns to Java.
  1. 全局jobjectjclass在任何 Java 线程中都有效。
  2. 本地对象仅在JNIEnv调用线程的一次函数调用中有效,并在 JNI 代码返回 Java 时被垃圾收集。

Now it depends on the threads that you are using: If you register your C++ function with env->RegisterNatives()and Java code is calling into your JNI functions then you must store all objects, that you want to use later, as global objects otherwise they will get garbage collected.

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

But if you create your own thread with the CraeteThread()API (on Windows) and obtain the JNIEnvstructure by calling AttachCurrentThreadAsDaemon()then completely other rules will apply: As it is your own thread, that never returns control to Java, the garbage collector will never clean up objects that you have created on your thread and you can even store local objects in static C++ variables without problems (but these cannot be accessed from other threads). In this case it is extremely important that you clean up ALL your local instances manually with env->DeleteLocalRef()otherwise you will have a memory leak. (However, the garbage collector probably frees all local objects when you call DetachCurrentThread(), which I never have tested.)

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

I strongly recommend to load ALL local objects into a wrapper class that calls DeleteLocalRef()in it's destructor. This is a bullet proof way to avoid memory leaks.

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



Developing JNI code may be very cumbersome and you may get crashes that you don't understand. To find the cause of a crash open a DOS window and start your Java application with the following command:

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

java -Xcheck:jni -jar MyApplication.jar

then you will see what problems have happened in JNI code. For example:

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

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

and you will find the stacktrace in the log file that Java creates in the same folder where you have the JAR file:

并且您将在 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...