在另一个 JNI 函数中使用时 Oop 被破坏

Oop gets corrupted when using in another JNI function

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

当我试图从另一个 JNI 方法调用中使用缓存的 jclassjmethodID 创建某个特定 class 的对象时,我遇到了一些奇怪的行为。

这是一个简单的例子:

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

JNI 函数所做的只是创建 class MyClass 的对象。函数 doSomeAction return 指向缓存的 jclassjmethodID 的指针。下面是原生方法的实现:

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

问题是当试图在 Java_com_test_Main_doAnotherAction 中创建对象时取消引用 0 时程序崩溃。崩溃发生在 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   

rdi这里好像包含了一个指向相关Oop的指针。我注意到的是前 5 次没有发生崩溃:

rdi            0x7191eb228

崩溃案例是

rdi            0x7191eb718

导致 0x0 被 return 编辑并崩溃。

在不同的 JNI 函数中使用 jclassjmethodID 时,Oop 会损坏什么?如果我使用本地找到的 jclassjmethodID 创建对象,一切正常。

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

mov    rdi,r13
#...
mov    rdi,QWORD PTR [rdi]

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

跨 JNI 调用缓存 jclass 是一个重大的 () 错误。
jclassjobjectspecial case - 它是一个 JNI 引用,应该被管理。

作为 JNI 规范 saysJNI 函数返回的所有 Java 对象都是本地引用。 因此,FindClass returns 本地 JNI 引用在本地方法 returns 后立即失效。也就是说,如果对象被移动,GC 将不会更新引用,或者另一个 JNI 调用可能会为不同的 JNI 引用重用相同的槽。

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

jthreadjstringjarrayjobjects的其他例子,也应该管理

JNIEnv*也不能缓存,因为它只在in the current thread.

有效

同时 jmethodIDjfieldID 可以在 JNI 调用中安全地重用 - 它们在 JVM 中明确标识一个 method/field 并且 intended for repeated use 只要持有人 class 还活着。但是,如果持有者 class 恰好被垃圾回收,它们也可能会失效。