静态 C++ 对象中的 JNI 环境指针并连续两次调用采用字符串参数的 java 函数会使 JVM 崩溃

2022-01-16 00:00:00 jvm c++ java java-native-interface

所以根据我的评论员要求,我终于找到了一个重现我的错误的 MCVE.所以一般设置是 Java 使用 JNI 调用 dll,而 dll 抓住正在运行的 JVM 并存储指向 JNIEnv 的指针,它用于调用 java 类中的方法(从 c++ 调用的 java 类不一定是原始调用 java 对象,这就是为什么输入对象不用于回调的原因).在我进一步解释之前,让我发布所有代码:

So on my commentators request I have finally found an MCVE that reproduces my error. So the general setup is that Java uses JNI to call into a dll, and the dll grabs hold of the running JVM and stores a pointer to the JNIEnv, which it uses to call methods in a java class (the java class being called from c++ is not necessarily the original calling java object, which is why the input jobject is not used for the callbacks). Before I explain any further just let me post all the code:

JniTest.java

package jnitest;

public class JniTestJava {
  public static void main(String[] args) {

    try {
      System.load("<path-to-dll>");
    } catch (Throwable e) {
      e.printStackTrace();
    }

    DllFunctions dllFunctions = new DllFunctions();
    dllFunctions.setup();
    dllFunctions.singleIntFunctionCall();
    dllFunctions.doubleIntFunctionCall();
    dllFunctions.singleStringFunctionCall();
    dllFunctions.doubleStringFunctionCall();
  }

  public void javaStringFunction(String input){
    System.out.println(input);
  }

  public void javaIntFunction(int input){
    System.out.println(input);
  }
}

DllFunctions.java

package jnitest;

public class DllFunctions{
  public native void singleIntFunctionCall();
  public native void doubleIntFunctionCall();
  public native void singleStringFunctionCall();
  public native void doubleStringFunctionCall();

  public native void setup();
}

JniTestCpp.h

#include <jni.h>
#ifndef _Included_jnitest_JniTestJava
#define _Included_jnitest_JniTestJava
#ifdef __cplusplus

extern "C" {
#endif
  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_setup(JNIEnv* java_env, jobject);
  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleIntFunctionCall(JNIEnv* java_env, jobject);
  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleIntFunctionCall(JNIEnv* java_env, jobject);
  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleStringFunctionCall(JNIEnv* java_env, jobject);
  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleStringFunctionCall(JNIEnv* java_env, jobject);

#ifdef __cplusplus
}
#endif
#endif

JniTestCpp.cpp

#include "JniTestCpp.h"
#include "JniTestClass.h"

JniTestClass jniTestClass;
extern "C"
{
  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_setup(JNIEnv* java_env, jobject) {
    jniTestClass.setup();
  }

  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleIntFunctionCall(JNIEnv* java_env, jobject) {
    jniTestClass.callJavaIntFunction();
  }

  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleIntFunctionCall(JNIEnv* java_env, jobject) {
    jniTestClass.callJavaIntFunction();
    jniTestClass.callJavaIntFunction();
  }

  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleStringFunctionCall(JNIEnv* java_env, jobject) {
    jniTestClass.callJavaStringFunction();
  }

  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleStringFunctionCall(JNIEnv* java_env, jobject) {
    jniTestClass.callJavaStringFunction();
    jniTestClass.callJavaStringFunction();
  }
}

JniTestClass.h

#include <jni.h>

class JniTestClass {
  typedef jint(JNICALL * GetCreatedJavaVMs)(JavaVM**, jsize, jsize*);
public:
  void setup();
  void callJavaStringFunction();
  void callJavaIntFunction();
  void throwException(jthrowable ex);

private:
  jobject myObject;
  jclass myClass;
  JNIEnv* env;
};

JniTestClass.cpp

#include "JniTestClass.h"
#include <Windows.h>
#include <fstream>

void JniTestClass::setup() {
  jint jni_version = JNI_VERSION_1_4;
  GetCreatedJavaVMs jni_GetCreatedJavaVMs;
  jsize nVMs = 0;

  jni_GetCreatedJavaVMs = (GetCreatedJavaVMs) GetProcAddress(GetModuleHandle(
    TEXT("jvm.dll")), "JNI_GetCreatedJavaVMs");
  jni_GetCreatedJavaVMs(NULL, 0, &nVMs); 
  JavaVM** buffer = new JavaVM*[nVMs];
  jni_GetCreatedJavaVMs(buffer, nVMs, &nVMs); 
  buffer[0]->GetEnv((void **) &env, jni_version);
  delete buffer;

  myClass = env->FindClass("jnitest/JniTestJava");
  myObject = env->NewObject(myClass, env->GetMethodID(myClass, "<init>", "()V"));
}

void JniTestClass::callJavaStringFunction() {
  jmethodID myMethod = env->GetMethodID(myClass, "javaStringFunction", "(Ljava/lang/String;)V");
  if (env->ExceptionCheck()) {
    throwException(env->ExceptionOccurred());
  }

  env->CallVoidMethod(myObject, myMethod, env->NewStringUTF("String!"));
  if (env->ExceptionCheck()) {
    throwException(env->ExceptionOccurred());
  }
}

void JniTestClass::callJavaIntFunction() {
  jmethodID myMethod = env->GetMethodID(myClass, "javaIntFunction", "(I)V");
  if (env->ExceptionCheck()) {
    throwException(env->ExceptionOccurred());
  }

  env->CallVoidMethod(myObject, myMethod, 1);
  if (env->ExceptionCheck()) {
    throwException(env->ExceptionOccurred());
  }
}

void JniTestClass::throwException(jthrowable ex) {
  env->ExceptionClear();
  jclass clazz = env->GetObjectClass(ex);
  jmethodID getMessage = env->GetMethodID(clazz,
                                          "toString",
                                          "()Ljava/lang/String;");
  jstring message = (jstring) env->CallObjectMethod(ex, getMessage);
  const char *mstr = env->GetStringUTFChars(message, NULL);

  printf("%s 
", mstr);
  throw std::runtime_error(mstr);
}

这里的意图是 JniTestCpp 应该只有 JNI 导出函数并且没有声明的类.JniTestClass 背后的想法是它应该保存所有 JNI 指针和变量(对象、类和环境指针)并提供 JniTestCpp 可以使用的方法.

The intent here is that JniTestCpp should only have JNI exported functions and no declared classes. The idea behind the JniTestClass is that it should hold all the JNI pointers and variables (object, class and environment pointer) and provide methods that JniTestCpp can use.

现在,这段代码的呈现方式在 JniTest.java 中的 dllFunctions.doubleStringFunctionCall(); 调用中崩溃,输出如下:

Now, the way this code is presented it crashes on the dllFunctions.doubleStringFunctionCall(); call in JniTest.java with the following output:

1
1
1
String!
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x6e306515, pid=1268, tid=8028
#
# JRE version: Java(TM) SE Runtime Environment (7.0_80-b15) (build 1.7.0_80-b15)
# Java VM: Java HotSpot(TM) Client VM (24.80-b11 mixed mode, sharing windows-x86 )
# Problematic frame:
# V  [jvm.dll+0xc6515]

下面我展示了 hs_err_pidXXX.log 文件中的 10 个顶部堆栈帧:

and below I have shown the 10 top stack frames from the hs_err_pidXXX.log file:

Stack: [0x02150000,0x021a0000],  sp=0x0219f49c,  free space=317k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V  [jvm.dll+0xc6515]
V  [jvm.dll+0xc66c9]
C  [JniTestCpp.dll+0x13d52]  JNIEnv_::GetMethodID+0x42
C  [JniTestCpp.dll+0x14ecf]  JniTestClass::callJavaStringFunction+0x3f
C  [JniTestCpp.dll+0x16068]      Java_jnitest_DllFunctions_doubleStringFunctionCall+0x28
j  jnitest.DllFunctions.doubleStringFunctionCall()V+0
j  jnitest.JniTestJava.main([Ljava/lang/String;)V+38
v  ~StubRoutines::call_stub
V  [jvm.dll+0x1429aa]
V  [jvm.dll+0x20743e]

让我吃惊的是,如果我在 JniTestCpp.cpp 中没有将 JniTestClass jniTestClass 声明为静态对象,而是声明它并调用 setup() 在如下所示的每个方法中,它不会崩溃,但会产生预期的结果.另外,我必须说,我在调用 doubleIntFunctionCall(); 而不是 doubleStringFunctionCall();

What suprises me is that if I, in JniTestCpp.cpp don't declare JniTestClass jniTestClass as a static object, but rather declare it and call setup() on it in each method like shown below, it does not crash, but produces the expected results. Also, I must say that it is rather strange that i works when calling doubleIntFunctionCall(); but not doubleStringFunctionCall();

JniTestCpp.cpp - 这不会崩溃

#include "JniTestCpp.h"
#include "JniTestClass.h"

extern "C"
{
  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_setup(JNIEnv* java_env, jobject) {
    JniTestClass jniTestClass;
    jniTestClass.setup();
  }

  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleIntFunctionCall(JNIEnv* java_env, jobject) {
    JniTestClass jniTestClass;
    jniTestClass.setup();
    jniTestClass.callJavaIntFunction();
  }

  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleIntFunctionCall(JNIEnv* java_env, jobject) {
    JniTestClass jniTestClass;
    jniTestClass.setup();
    jniTestClass.callJavaIntFunction();
    jniTestClass.callJavaIntFunction();
  }

  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleStringFunctionCall(JNIEnv* java_env, jobject) {
    JniTestClass jniTestClass;
    jniTestClass.setup();
    jniTestClass.callJavaStringFunction();
  }

  JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleStringFunctionCall(JNIEnv* java_env, jobject) {
    JniTestClass jniTestClass;
    jniTestClass.setup();
    jniTestClass.callJavaStringFunction();
    jniTestClass.callJavaStringFunction();
  }
}

很抱歉,这篇文章很长,但这是我觉得我能够明确提出我的问题的唯一方式.

Sorry for the long post, but this was the only way I felt like I was able to unambiguously present my problem.

更新

在函数 void JniTestClass::callJavaStringFunction() 中,如果我将其更改为以下内容:

In function void JniTestClass::callJavaStringFunction(), if i change it to the following:

void JniTestClass::callJavaStringFunction() {
  jmethodID myMethod = env->GetMethodID(myClass, "javaStringFunction", "(Ljava/lang/String;)V");
  if (env->ExceptionCheck()) {
    throwException(env->ExceptionOccurred());
  }

  jstring j_string = env->NewStringUTF("String!");
  env->CallVoidMethod(myObject, myMethod, j_string);
  if (env->ExceptionCheck()) {
    throwException(env->ExceptionOccurred());
  }

  env->DeleteLocalRef(j_string);
}

我现在在 NewStringUTF() 创建的 jstring 上调用 DeleteLocalRef(),程序仍然崩溃但打印出此异常消息:

where I now call DeleteLocalRef() on the jstring created with NewStringUTF(), the program still crashes but prints out this exception message:

java.lang.NoSuchMethodError: javaStringFunction

推荐答案

你的代码有几个错误.

  1. jobject myObjectjclass myClass 在 JNI 调用中重复使用.

  1. jobject myObject and jclass myClass are reused across JNI calls.

默认情况下,在 JNI 方法中创建的所有 jobjects 都是本地引用.每当 JNI 方法返回时,所有本地引用都会自动释放.

All jobjects created inside a JNI method are local references by default. Whenever a JNI method returns, all local references are automatically released.

如果你想在方法调用之间重用 jobject(或 jclass,它也是一个对象引用),你应该使用 NewGlobalRef.当不再需要全局引用时,应通过 DeleteGlobalRef,否则被引用的对象永远不会被垃圾回收.

If you want to reuse jobject (or jclass which is also an object reference) across method calls, you should convert it to a global reference using NewGlobalRef. When a global reference is no longer needed, it should be deleted by DeleteGlobalRef, otherwise referenced object will never be garbage-collected.

JNIEnv* 已缓存.

一般来说,JNIEnv* 永远不应该被存储以供以后重用.相反,您应该使用作为每个 JNI 函数的第一个参数提供的 JNIEnv*.或者,它可以通过 GetEnv 调用.请注意,每个线程都有自己的 JNIEnv*,不适用于其他线程.

In general, JNIEnv* should never be stored for later reuse. Instead you should use JNIEnv* provided as the first argument to each JNI function. Alternatively it may be obtained by GetEnv call. Note that each thread has its own JNIEnv* which is not applicable to other threads.

相关文章