在另一个JNI函数中使用时,Oop会损坏

St.*_*rio 1 java java-native-interface jvm jvm-hotspot

问题是我们可以缓存jclassjmethodID跨不同的JNI方法调用吗?

尝试通过缓存jclassjmethodID其他JNI方法调用创建某个特定类的对象时,遇到了一些奇怪的行为。

这是一个简单的示例:

public class Main {
    static {
        System.loadLibrary("test-crash");
    }

    public static void main(String args[]) throws InterruptedException {
        Thread.sleep(20000);
        doAnotherAction(doSomeAction());
    }

    private static native long doSomeAction();

    private static native void doAnotherAction(long ptr);
}

public class MyClass {
    public int a;

    public MyClass(int a) {
        if(a == 10){
            throw new IllegalArgumentException("a == 10");
        }
        this.a = a;
    }
}
Run Code Online (Sandbox Code Playgroud)

JNI函数所做的只是创建类的对象MyClass。该函数doSomeAction返回一个指向已缓存的jclass和的指针jmethodID。这是本机方法的实现:

struct test{
    jclass mc;
    jmethodID ctor;
};

JNIEXPORT jlong JNICALL Java_com_test_Main_doSomeAction
  (JNIEnv *env, jclass jc){
  (void) jc;

  jclass mc = (*env)->FindClass(env, "com/test/MyClass");
  jmethodID ctor = (*env)->GetMethodID(env, mc, "<init>", "(I)V");

  struct test *test_ptr = malloc(sizeof *test_ptr);
  test_ptr->mc = mc;
  test_ptr->ctor = ctor;

  printf("Creating element0\n");
  jobject ae1 = (*env)->NewObject(env, test_ptr->mc, test_ptr->ctor, (jint) 0);
  (void) ae1;

  printf("Creating element0\n");
  jobject ae2 = (*env)->NewObject(env, test_ptr->mc, test_ptr->ctor, (jint) 0);
  (void) ae2;

  printf("Creating element0\n");
  jobject ae3 = (*env)->NewObject(env, test_ptr->mc, test_ptr->ctor, (jint) 0);
  (void) ae3;

  return (intptr_t) test_ptr;
}

JNIEXPORT void JNICALL Java_com_test_Main_doAnotherAction
  (JNIEnv *env, jclass jc, jlong ptr){
  (void) jc;

  struct test *test_ptr= (struct test *) ptr;
  jclass mc = test_ptr->mc;
  jmethodID ctor = test_ptr->ctor;

  printf("Creating element\n");
  jobject ae1 = (*env)->NewObject(env, mc, ctor, (jint) 0);
  (void) ae1;

  printf("Creating element\n");
  jobject ae2 = (*env)->NewObject(env, mc, ctor, (jint) 0);
  (void) ae2;

  printf("Creating element\n");
  jobject ae3 = (*env)->NewObject(env, mc, ctor, (jint) 0); //CRASH!!
  (void) ae3;
}
Run Code Online (Sandbox Code Playgroud)

问题是0尝试在中创建对象时取消引用时程序崩溃Java_com_test_Main_doAnotherAction。在object_alloc函数调用时发生崩溃java_lang_Class::as_Klass(oopDesc*)

令人厌恶的java_lang_Class::as_Klass(oopDesc*)

Dump of assembler code for function _ZN15java_lang_Class8as_KlassEP7oopDesc:                                                                                                                                       
   0x00007f7f6b02eeb0 <+0>:     movsxd rax,DWORD PTR [rip+0x932ab5]        # 0x7f7f6b96196c <_ZN15java_lang_Class13_klass_offsetE>                                                                                 
   0x00007f7f6b02eeb7 <+7>:     push   rbp                                                                                                                                                                         
   0x00007f7f6b02eeb8 <+8>:     mov    rbp,rsp                                                                                                                                                                     
   0x00007f7f6b02eebb <+11>:    pop    rbp                                                                                                                                                                         
   0x00007f7f6b02eebc <+12>:    mov    rax,QWORD PTR [rdi+rax*1]                                                                                                                                                   
   0x00007f7f6b02eec0 <+16>:    ret   
Run Code Online (Sandbox Code Playgroud)

rdi这里似乎包含一个指向相关的指针Oop。我注意到的是前5次未发生崩溃的情况:

rdi            0x7191eb228
Run Code Online (Sandbox Code Playgroud)

崩溃的情况是

rdi            0x7191eb718
Run Code Online (Sandbox Code Playgroud)

导致0x0退货并崩溃。

是什么让Oop使用时损坏jclass,并jmethodID在不同的JNI功能呢?如果我创建对象与本地发现jclassjmethodID一切工作就好了。

UPD:分析核心转储后,我发现rdi的加载方式为

mov    rdi,r13
#...
mov    rdi,QWORD PTR [rdi]
Run Code Online (Sandbox Code Playgroud)

虽然r13似乎在我的JNI函数中没有更新...

apa*_*gin 5

jclass跨JNI调用进行缓存是一个主要(尽管很典型)错误。
jclass是一个特例jobject-这是一个JNI参考,并应加以管理。

正如JNI规范所说JNI函数返回的所有Java对象都是本地引用。因此,FindClass返回本地JNI引用,该引用将在本机方法返回后立即失效。也就是说,如果移动了对象,GC将不会更新引用,否则另一个JNI调用可能会将同一插槽重新用于其他JNI引用。

为了缓存jclassJNI调用,您可以使用NewGlobalRef函数将其转换为全局引用。

jthreadjstringjarray是其他例子jobjects,他们也应加以管理。

JNIEnv*也不要缓存,因为它仅在当前线程中有效。

同时jmethodID,它们jfieldID可以在JNI调用之间安全地重用-它们明确标识JVM中的方法/字段,并且只要Holder类还活着,它们就可以重复使用。但是,如果holder类碰巧被垃圾回收,它们也可能变得无效。