JNI官方规范中文版——如何使用JNI中的global reference和local reference

本文详细介绍了JNI引用管理的规则与实践,包括局部引用、全局引用、弱引用的生命周期与释放方法,以及如何避免内存溢出。通过具体代码示例,指导开发者合理管理JNI引用,提高代码质量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载地址:http://blog.youkuaiyun.com/a345017062/article/details/8068928


JNI提供了一些实例和数组类型(jobject、jclass、jstring、jarray等)作为不透明的引用供本地代码使用。本地代码永远不会直接操作引用指向的VM内部的数据内容。要进行这些操作,必须通过使用JNI操作一个不引用来间接操作数据内容。因为只操作引用,你不必担心特定JVM中对象的存储方式等信息。这样的话,你有必要了解一下JNI中的几种不同的引用:

1、 JNI支持三种引用:局部引用、全局引用、弱全局引用(下文简称“弱引用”)。

2、 局部引用和全局引用有不同的生命周期。当本地方法返回时,局部引用会被自动释放。而全局引用和弱引用必须手动释放。

3、 局部引用或者全局引用会阻止GC回收它们所引用的对象,而弱引用则不会。

4、 不是所有的引用可以被用在所有的场合。例如,一个本地方法创建一个局部引用并返回后,再对这个局部引用进行访问是非法的。

本章中,我们会详细地讨论这些问题。合理地管理JNI引用是写出高质量的代码的基础。

5.1 局部引用和全局引用

什么是全局引用和局部引用?它们有什么不同?我们下面使用一些例子来说明。

5.1.1 局部引用

大多数JNI函数会创建局部引用。例如,NewObject创建一个新的对象实例并返回一个对这个对象的局部引用。

局部引用只有在创建它的本地方法返回前有效。本地方法返回后,局部引用会被自动释放。

你不能在本地方法中把局部引用存储在静态变量中缓存起来供下一次调用时使用。下面的例子是MyNewString函数的一个修改版本,这里面使用局部引用的方法是错误的:

/* This code is illegal */

 jstring

 MyNewString(JNIEnv *env, jchar *chars, jint len)

 {

     static jclass stringClass = NULL;

     jmethodID cid;

     jcharArray elemArr;

     jstring result;

 

     if (stringClass == NULL) {

         stringClass = (*env)->FindClass(env,

                                         "java/lang/String");

         if (stringClass == NULL) {

             return NULL; /* exception thrown */

         }

     }

     /* It is wrong to use the cached stringClass here,

        because it may be invalid. */

     cid = (*env)->GetMethodID(env, stringClass,

                               "<init>", "([C)V");

     ...

     elemArr = (*env)->NewCharArray(env, len);

     ...

     result = (*env)->NewObject(env, stringClass, cid, elemArr);

     (*env)->DeleteLocalRef(env, elemArr);

     return result;

 }

上面代码中,我们省略了和我们的讨论无关的代码。因为FindClass返回一个对java.lang.String对象的局部引用,上面的代码中缓存stringClassr做法是错误的。假设一个本地方法C.f调用了MyNewString:

JNIEXPORT jstring JNICALL

 Java_C_f(JNIEnv *env, jobject this)

 {

     char *c_str = ...;

     ...

     return MyNewString(c_str);

 }

C.f方法返回后,VM释放了在这个方法执行期间创建的所有局部引用,也包含对String类的引用stringClass。当再次调用MyNewString时,会试图访问一个无效的局部引用,从而导致非法的内存访问甚至系统崩溃。

释放一个局部引用有两种方式,一个是本地方法执行完毕后VM自动释放,另外一个是程序员通过DeleteLocalRef手动释放。

既然VM会自动释放局部引用,为什么还需要手动释放呢?因为局部引用会阻止它所引用的对象被GC回收。

局部引用只在创建它们的线程中有效,跨线程使用是被禁止的。不要在一个线程中创建局部引用并存储到全局引用中,然后到另外一个线程去使用。

5.1.2 全局引用

全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用一样,全局引用也会阻止它所引用的对象被GC回收。

与局部引用可以被大多数JNI函数创建不同,全局引用只能使用一个JNI函数创建:NewGlobalRef。下面这个版本的MyNewString演示了怎么样使用一个全局引用:

/* This code is OK */

 jstring

 MyNewString(JNIEnv *env, jchar *chars, jint len)

 {

     static jclass stringClass = NULL;

     ...

     if (stringClass == NULL) {

         jclass localRefCls =

             (*env)->FindClass(env, "java/lang/String");

         if (localRefCls == NULL) {

             return NULL; /* exception thrown */

         }

         /* Create a global reference */

         stringClass = (*env)->NewGlobalRef(env, localRefCls);

 

         /* The local reference is no longer useful */

         (*env)->DeleteLocalRef(env, localRefCls);

 

         /* Is the global reference created successfully? */

         if (stringClass == NULL) {

             return NULL; /* out of memory exception thrown */

         }

     }

     ...

 }

上面这段代码中,一个由FindClass返回的局部引用被传入NewGlobalRef,用来创建一个对String类的全局引用。删除localRefCls后,我们检查NewGlobalRef是否成功创建stringClass。

5.1.3 弱引用

弱引用使用NewGlobalWeakRef创建,使用DeleteGlobalWeakRef释放。与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象。

在MyNewString中,我们也可以使用弱引用来存储stringClass这个类引用,因为java.lang.String这个类是系统类,永远不会被GC回收。

当本地代码中缓存的引用不一定要阻止GC回收它所指向的对象时,弱引用就是一个最好的选择。假设,一个本地方法mypkg.MyCls.f需要缓存一个指向类mypkg.MyCls2的引用,如果在弱引用中缓存的话,仍然允许mypkg.MyCls2这个类被unload:

JNIEXPORT void JNICALL

 Java_mypkg_MyCls_f(JNIEnv *env, jobject self)

 {

     static jclass myCls2 = NULL;

     if (myCls2 == NULL) {

         jclass myCls2Local =

             (*env)->FindClass(env, "mypkg/MyCls2");

         if (myCls2Local == NULL) {

             return; /* can't find class */

         }

         myCls2 = NewWeakGlobalRef(env, myCls2Local);

         if (myCls2 == NULL) {

             return; /* out of memory */

         }

     }

     ... /* use myCls2 */

 }

我们假设MyCls和MyCls2有相同的生命周期(例如,他们可能被相同的类加载器加载),因为弱引用的存在,我们不必担心MyCls和它所在的本地代码在被使用时,MyCls2这个类出现先被unload,后来又会preload的情况。

当然,真的发生这种情况时(MyCls和MyCls2的生命周期不同),我们必须检查缓存过的弱引用是指向活动的类对象,还是指向一个已经被GC给unload的类对象。下一节将告诉你怎么样检查弱引用是否活动。

5.1.4 引用比较

给定两个引用(不管是全局、局部还是弱引用),你可以使用IsSameObject来判断它们两个是否指向相同的对象。例如:

(*env)->IsSameObject(env, obj1, obj2)

如果obj1和obj2指向相同的对象,上面的调用返回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0)。

JNI中的一个引用NULL指向JVM中的null对象。如果obj是一个局部或者全局引用,你可以使用(*env)->IsSameObject(env, obj, NULL)或者obj == NULL来判断obj是否指向一个null对象。

在这一点儿上,弱引用有些有同,一个NULL弱引用同样指向一个JVM中的null对象,但不同的是,在一个弱引用上面使用IsSameObject时,返回值的意义是不同的:

(*env)->IsSameObject(env, wobj, NULL)

上面的调用中,如果wobj已经被回收,会返回JNI_TRUE,如果wobj仍然指向一个活动对象,会返回JNI_FALSE。

5.2 释放引用

每一个JNI引用被建立时,除了它所指向的JVM中的对象以外,引用本身也会消耗掉一个数量的内存。作为一个JNI程序员,你应该对程序在一个给定时间段内使用的引用数量十分小心。短时间内创建大量不会被立即回收的引用会导致内存溢出。

5.2.1 释放局部引用

大部分情况下,你在实现一个本地方法时不必担心局部引用的释放问题,因为本地方法被调用完成后,JVM会自动回收这些局部引用。尽管如此,以下几种情况下,为了避免内存溢出,JNI程序员应该手动释放局部引用:

1、 在实现一个本地方法调用时,你需要创建大量的局部引用。这种情况可能会导致JNI局部引用表的溢出,所以,最好是在局部引用不需要时立即手动删除。比如,在下面的代码中,本地代码遍历一个大的字符串数组,每遍历一个元素,都会创建一个局部引用,当对这个元素的遍历完成时,这个局部引用就不再需要了,你应该手动释放它:

for (i = 0; i < len; i++) {

     jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);

     ... /* process jstr */

     (*env)->DeleteLocalRef(env, jstr);

 }

2、 你想写一个工具函数,这个函数被谁调用你是不知道的。4.3节中的MyNewString演示了怎么样在工具函数中使用引用后,使用DeleteLocalRef删除。不这样做的话,每次MyNewString被调用完成后,就会有两个引用仍然占用空间。

3、 你的本地方法不会返回任何东西。例如,一个本地方法可能会在一个事件接收循环里面被调用,这种情况下,为了不让局部引用累积造成内存溢出,手动释放也是必须的。

4、 你的本地方法访问一个大对象,因此创建了一个对这个大对象的引用。然后本地方法在返回前会有一个做大量的计算过程,而在这个过程中是不需要前面创建的对大对象的引用的。但是,计算过程,对大对象的引用会阻止GC回收大对象。

在下面的程序中,因为预先有一个明显的DeleteLocalRef操作,在函数lengthyComputation的执行过程中,GC可能会释放由引用lref指向的对象。

5.2.2 管理局部引用

JDK提供了一系列的函数来管理局部引用的生命周期。这些函数包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame。

JNI规范中指出,VM会确保每个本地方法可以创建至少16个局部引用。经验表明,这个数量已经满足大多数不需要和JVM中的内部对象有太多交互的本地方法。如果真的需要创建更多的引用,本地方法可以通过调用EnsureLocalCapacity来支持更多的局部引用。在下面的代码中,对前面的例子做了些修改,不考虑内存因素的情况下,它可以为创建大量的局部引用提供足够的空间。

· /* The number of local references to be created is equal to

·     the length of the array. */ 

·  if ((*env)->EnsureLocalCapacity(env, len)) < 0) {

·      ... /* out of memory */

·  }

·  for (i = 0; i < len; i++) {

·      jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);

·      ... /* process jstr */

·      /* DeleteLocalRef is no longer necessary */

·  }

当然,上面这个版本中没有立即删除不使用的局部引用,因此会比前面的版本消耗更多的内存。

另外,Push/PopLocalFrame函数对允许程序员创建作用范围层层嵌套的局部引用。例如,我们可以把上面的代码重写:

· #define N_REFS ... /* the maximum number of local references

·                        used in each iteration */

·  for (i = 0; i < len; i++) {

·      if ((*env)->PushLocalFrame(env, N_REFS) < 0) {

·          ... /* out of memory */

·      }

·      jstr = (*env)->GetObjectArrayElement(env, arr, i);

·      ... /* process jstr */

·      (*env)->PopLocalFrame(env, NULL);

·  }

PushLocalFrame为一定数量的局部引用创建了一个使用堆栈,而PopLocalFrame负责销毁堆栈顶端的引用。

Push/PopLocalFrame函数对提供了对局部引用的生命周期更方便的管理。上面的例子中,如果处理jstr的过程中创建了局部引用,则PopLocalFrame执行时,这些局部引用全部会被销毁。

当你写一个会返回局部引用的工具函数时,NewLocalRef非常有用,我们会在5.3节中演示NewLocalRef的使用。

本地代码可能会创建大量的局部引用,其数量可能会超过16个或PushLocaFrame和EnsureLocalCapacity调用设置的个数。VM可能会尝试分配足够的内存,但不能够保证分配成功。如果失败,VM会退出。

5.2.3 释放全局引用

当你的本地代码不再需要一个全局引用时,你应该调用DeleteGlobalRef来释放它。如果你没有调用这个函数,即使这个对象已经没用了,JVM也不会回收这个全局引用所指向的对象。

当你的本地代码不再需要一个弱引用时,应该调用DeleteWeakGlobalRef来释放它,如果你没有调用这个函数,JVM仍会回收弱引用所指向的对象,但弱引用本身在引用表中所占的内存永远也不会被回收。

5.3 管理引用的规则

前面已经做了一个全面的介绍,现在我们可以总结一下JNI引用的管理规则了,目标就是减少内存使用和对象被引用保持而不能释放。

通常情况下,有两种本地代码:直接实现本地方法的本地代码和可以被使用在任何环境下的工具函数。

当编写实现本地方法的本地代码时,当心不要造成全局引用和弱引用的累加,因为本地方法执行完毕后,这两种引用不会被自动释放。

当编写一个工具函数的本地代码时,当心不要在函数的调用轨迹上面遗漏任何的局部引用,因为工具函数被调用的场合是不确定的,一旦被大量调用,很有可能造成内存溢出。

编写工具函数时,请遵守下面的规则:

1、 一个返回值为基本类型的工具函数被调用时,它决不能造成局部、全局、弱引用不被回收的累加。

2、 当一个返回值为引用类型的工具函数被调用时,它除了返回的引用以外,它决不能造成其它局部、全局、弱引用的累加。

对工具函数来说,为了使用缓存技术而创建一些全局引用或者弱引用是正常的。

如果一个工具函数返回一个引用,你应该详细说明返回的引用的类型,以便于调用者更好地管理它们。下面的代码中,频繁地调用工具函数GetInfoString,我们需要知道GetInfoString返回的引用的类型,以便于在每次使用完成后可以释放掉它:

· while (JNI_TRUE) {

·      jstring infoString = GetInfoString(info);

·      ... /* process infoString */

·      

·      ??? /* we need to call DeleteLocalRef, DeleteGlobalRef,

·             or DeleteWeakGlobalRef depending on the type of 

·             reference returned by GetInfoString. */

·  }

函数NewLocalRef有时被用来确保一个工具函数返回一个局部引用。为了演示这个用法,我们对MyNewString函数做了一些修改。下面的版本把一个被频繁调用的字符串“CommonString” 缓存在了全局引用里:

· jstring

·  MyNewString(JNIEnv *env, jchar *chars, jint len)

·  {

·      static jstring result;

·  

·      /* wstrncmp compares two Unicode strings */

·      if (wstrncmp("CommonString", chars, len) == 0) {

·          /* refers to the global ref caching "CommonString" */

·          static jstring cachedString = NULL;

·          if (cachedString == NULL) {

·              /* create cachedString for the first time */

·              jstring cachedStringLocal = ... ;

·             /* cache the result in a global reference */

·              cachedString = 

·                  (*env)->NewGlobalRef(env, cachedStringLocal);

·          }

·          return (*env)->NewLocalRef(env, cachedString);

·      }

·  

·      ... /* create the string as a local reference and store in

·             result as a local reference */

·      return result;

·  }

在管理局部引用的生命周期中,Push/PopLocalFrame是非常方便的。你可以在本地函数的入口处调用PushLocalFrame,然后在出口处调用PopLocalFrame,这样的话,在函数对中间任何位置创建的局部引用都会被释放。而且,这两个函数是非常高效的,强烈建议使用它们。

如果你在函数的入口处调用了PushLocalFrame,记住在所有的出口(有return出现的地方)调用PopLocalFrame。在下面的代码中,对PushLocalFrame的调用只有一次,但对PopLocalFrame的调用却需要多次。

· jobject f(JNIEnv *env, ...)

·  {

·      jobject result;

·      if ((*env)->PushLocalFrame(env, 10) < 0) {

·          /* frame not pushed, no PopLocalFrame needed */

·          return NULL; 

·      }

·      ...

·      result = ...;

·      if (...) {

·          /* remember to pop local frame before return */

·          result = (*env)->PopLocalFrame(env, result);

·          return result;

·      }

·      ...

·      result = (*env)->PopLocalFrame(env, result);

·      /* normal return */

·      return result;

·  }

上面的代码同样演示了函数PopLocalFrame的第二个参数的用法。局部引用result一开始在PushLocalFrame创建的当前frame里面被创建,而把result传入PopLocalFrame中时,PopLocalFrame在弹出当前的frame前,会由result生成一个新的局部引用,再把这个新生成的局部引用存储在上一个frame当中。


<think>好的,我现在需要回答用户关于JNI中jclass、jmethodIDjfieldID是否可以跨线程使用的问题。首先,我要回忆JNI的基本知识,特别是涉及多线程的部分。 用户之前询问过C++动态库的动态加载,现在转向JNI,可能是在开发涉及Java本地代码的应用,尤其是在多线程环境下。用户可能遇到了在多线程中使用JNI对象的问题,或者想要优化性能,避免重复获取这些ID。 首先,我需要确认JNI规范中对这些类型的跨线程使用的说明。JNI文档中提到,jclass、jmethodIDjfieldID这些对象是JVM内部的引用,它们的生命周期线程安全性是关键点。我记得jmethodIDjfieldID在获取后是有效的,直到对应的类被卸载,因此可以在不同线程中使用,但需要确保类没有被卸载。而jclass是局部引用,通常只在当前线程有效,不能直接跨线程使用,但可以通过创建全局引用来跨线程共享。 接下来,我需要考虑用户可能的误区。比如,用户可能误以为局部引用可以直接传给其他线程使用,但实际上这会导致未定义行为。需要强调必须使用NewGlobalRef来创建全局引用,并且在使用后需要删除,避免内存泄漏。 另外,线程安全方面,虽然jmethodIDjfieldID本身可以在多个线程中使用,但调用JNI函数时需要确保正确管理JNIEnv,因为每个线程都有自己的JNIEnv指针。需要提醒用户通过AttachCurrentThread获取当前线程的JNIEnv,而不是跨线程传递JNIEnv。 可能还需要举例子说明正确的做法,比如如何将jclass转换为全局引用,并在其他线程中使用。同时,强调错误处理的重要性,比如检查全局引用是否创建成功,及时释放资源以避免内存泄漏。 总结起来,回答的结构应该是先明确每个类型的跨线程使用情况,然后分点详细说明,包括生命周期、正确使用方法、注意事项示例。同时,要使用用户容易理解的语言,避免过于技术化的术语,但保持准确性。</think>在 JNIJava Native Interface)中,`jclass`、`jmethodID` `jfieldID` 的跨线程使用需要谨慎处理,具体规则如下: --- ### **1. `jclass`** - **能否跨线程使用**: **不能直接跨线程使用**。`jclass` 是一个**局部引用**(Local Reference),其生命周期默认与当前线程的 JNI 调用栈绑定。若跨线程传递原始 `jclass`,可能导致未定义行为(如崩溃或数据错误)。 - ✅ **解决方案**: 使用 `NewGlobalRef` 将其提升为**全局引用**(Global Reference),全局引用可以跨线程共享。 ```cpp jclass localClazz = env->FindClass("com/example/MyClass"); jclass globalClazz = (jclass)env->NewGlobalRef(localClazz); // 其他线程可通过 globalClazz 访问 ``` - ⚠️ **注意**: 全局引用需手动释放(`DeleteGlobalRef`),否则会导致内存泄漏。 --- ### **2. `jmethodID` `jfieldID`** - **能否跨线程使用**: **可以跨线程直接使用**,前提是满足以下条件: 1. 对应的 Java 类未被卸载(通常类卸载极少发生)。 2. `jmethodID`/`jfieldID` 是通过有效的 `jclass` 获取的(例如全局引用对应的类)。 - ✅ **直接跨线程示例**: ```cpp // 主线程获取 methodID jmethodID methodID = env->GetMethodID(globalClazz, "myMethod", "()V"); // 子线程直接使用 methodID otherEnv->CallVoidMethod(obj, methodID); ``` - ⚠️ **注意事项**: - `jmethodID` `jfieldID` 本质是 JVM 内部指针,由 JVM 保证其线程安全性。 - 若类被卸载(如通过自定义类加载器加载的类),对应的 ID 会失效,但通常这种情况极少发生。 --- ### **3. 跨线程使用 JNI 的通用规则** - **JNIEnv 的线程绑定**: `JNIEnv*`(环境指针)是**线程局部**的,不可跨线程直接传递。在子线程中需通过 `AttachCurrentThread` 获取当前线程的 `JNIEnv`: ```cpp JNIEnv* otherEnv; vm->AttachCurrentThread(&otherEnv, nullptr); // vm 是 JavaVM* 实例 ``` - **全局引用的释放**: 跨线程共享的全局引用(如 `jclass`)需在所有线程使用完毕后释放: ```cpp env->DeleteGlobalRef(globalClazz); ``` --- ### **4. 典型错误场景** 1. **直接跨线程传递局部 `jclass`**: ```cpp // 错误!局部 jclass 不能在子线程使用 jclass localClazz = env->FindClass("com/example/MyClass"); otherThreadUse(localClazz); // 崩溃风险 ``` 2. **未正确附加线程导致 `JNIEnv*` 为空**: ```cpp // 错误!子线程未调用 AttachCurrentThread JNIEnv* env; env->CallVoidMethod(obj, methodID); // 可能崩溃 ``` --- ### **总结** | 类型 | 跨线程使用条件 | 注意事项 | |---------------|--------------------------------------|-----------------------------| | `jclass` | 必须转为全局引用(`NewGlobalRef`) | 需手动释放全局引用 | | `jmethodID` | 可直接跨线程使用 | 确保类未被卸载 | | `jfieldID` | 可直接跨线程使用 | 同上 | 通过合理管理全局引用 `JNIEnv`,可以安全地在多线程环境中使用这些 JNI 对象。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值