JNI ERROR (app bug): accessed stale local reference

本文解析了在Android 4.0及更高版本中遇到的JNI ERROR(appbug): accessed stale local reference问题。该问题源于垃圾回收机制的变化导致的对象移动。文章提供了两种避免此错误的方法:一是将targetSdkVersion设置为11或更低;二是避免使用指向Java对象的静态变量或将jobject引用设为全局。

转载自:http://stackoverflow.com/questions/14765776/jni-error-app-bug-accessed-stale-local-reference-0xbc00021-index-8-in-a-tabl


JNI ERROR (app bug): accessed stale local reference

解决办法:

Since android 4.0 garbage collector was changed. Now it moves object around during garbage collection, which can cause a lot of problems.

Imagine that you have a static variable pointing to an object, and then this object gets moved by gc. Since android uses direct pointers for java objects, this would mean that your static variable is now pointing to a random address in the memory, unoccupied by any object or occupied by an object of different sort. This will almost guarantee that you'll get EXC_BAD_ACCESS next time you use this variable.

So android gives you JNI ERROR (app bug) error to prevent you from getting undebugable EXC_BAD_ACCESS. Now there are two ways to avoid this error.

  1. You can set targetSdkVersion in your manifest to version 11 or less. This will enable JNI bug compatibility mode and prevent any problems altogether. This is the reason why your old examples are working.

  2. You can avoid using static variables pointing to java objects or make jobject references global before storing them by calling env->NewGlobalRef(ref).
    Perhaps on of the biggest examples here is keeping jclass objects. Normally, you'll initialize static jclass variable during JNI_OnLoad, since class objects remain in the memory as long as the application is running.


<think>我们正在解决一个特定的JNI错误:JNI ERROR attempt to use stale WeakGlobal reference(尝试使用过时的弱全局引用)。这个错误通常发生在尝试访问一个已经被垃圾回收的弱全局引用所指向的Java对象时。 根据引用[1]:修复方法是只存储JNI全局引用。因为JNI全局引用永远不会自动清理,所以自己清理它们至关重要。但这里有一个问题:析构函数不会有JNIEnv*。最简单的修复方法通常是为本地对等体(native peer)设置一个显式的'destroy'函数,从Java对等体的终结器(finalizer)中调用。 然而,我们的问题涉及的是WeakGlobal引用(弱全局引用)而不是全局引用(GlobalRef)。弱全局引用的特点是:它不会阻止所引用对象被垃圾回收。因此,当我们尝试使用一个弱全局引用时,必须先检查它是否已经被回收(通过IsSameObject将弱全局引用与NULL比较)。如果被回收了,我们就不能再使用它。 错误原因分析: 1. 可能我们在使用弱全局引用之前没有检查它是否已经失效(即被垃圾回收)。 2. 可能我们在使用弱全局引用时,没有正确地将其转换为强引用(通过NewGlobalRef或NewLocalRef)来保证在使用期间对象不被回收。 解决方案: 步骤1:在使用弱全局引用之前,先检查它是否已经失效。 步骤2:如果未失效,将其转换为局部引用或全局引用(临时强引用)来使用,确保在使用过程中对象不会被垃圾回收。 步骤3:使用完毕后,及时删除创建的临时强引用(如果是局部引用,在本地方法返回后会自动释放;如果是全局引用,需要显式删除),但保留原来的弱全局引用(因为它可能还会在其他地方使用)。 示例代码: 假设我们有一个弱全局引用g_weakRef,我们想在C++代码中调用它的方法。 正确使用方式: ```c++ JNIEnv* env = ...; // 获取当前线程的JNIEnv jobject weakRef = g_weakRef; // 弱全局引用 jobject strongRef = env->NewGlobalRef(weakRef); // 尝试创建强引用 if (strongRef == NULL) { // 弱引用已经被回收,需要重新获取对象或者处理为无效 } else { // 使用strongRef安全地调用Java方法 jclass clazz = env->GetObjectClass(strongRef); jmethodID method = env->GetMethodID(clazz, "method", "()V"); env->CallVoidMethod(strongRef, method); // 使用完后,删除强引用 env->DeleteGlobalRef(strongRef); } ``` 但是,注意:上面的例子中,我们使用NewGlobalRef来创建一个强引用。如果对象已经被回收,NewGlobalRef会返回NULL。因此,我们通过检查strongRef是否为NULL来判断弱引用是否有效。 另外,我们也可以使用NewLocalRef,但是要注意局部引用只能在当前线程的当前本地方法调用中使用,并且有数量限制(可以通过PushLocalFrame/PopLocalFrame来管理)。如果我们在一个可能会被多次调用的函数中(比如一个循环中),使用全局引用可能更合适,但要注意及时删除。 然而,上面的方法每次使用都创建一个全局引用,然后立即删除,可能会造成性能开销。另一种方法是使用局部引用: ```c++ jobject strongRef = env->NewLocalRef(weakRef); if (strongRef == NULL) { // 已经被回收 } else { // 使用strongRef // ... // 不需要显式删除局部引用,除非在大量创建局部引用的循环中,这时可以手动调用DeleteLocalRef,或者使用Push/PopLocalFrame // 但通常在这个作用域结束(函数返回)时,局部引用会被自动释放。 } ``` 重要提示:在Android JNI中,弱全局引用必须通过IsSameObject(env, weakRef, NULL)来检查吗?实际上,使用NewLocalRef或NewGlobalRef来尝试提升为强引用,如果返回NULL,就表示弱引用已经被回收。这是推荐的方式。 另外,根据引用[5]:虽然任何一个JNIEnv*只在一个线程中有效,但现在Android在每个线程的JNIEnv*中维护了一个本地引用表,所以必须确保只在正确的线程中使用JNIEnv*。因此,我们在使用弱全局引用时,也必须在正确的线程中(即已经AttachCurrentThread的线程)使用。 防止弱引用过时(stale)的最佳实践: 1. 当不再需要弱全局引用时,应该调用DeleteWeakGlobalRef显式删除它,避免持有无用引用。 2. 在每次使用弱全局引用之前,都要检查它是否有效(通过尝试提升为强引用并检查是否为NULL)。 错误修复步骤: 1. 在代码中找到所有使用弱全局引用的地方。 2. 在使用前,先尝试将其提升为强引用(局部或全局),并检查提升是否成功(即强引用非NULL)。 3. 如果提升失败,说明对象已经被回收,需要处理这种情况(比如重新获取对象,或者跳过调用等)。 4. 如果提升成功,则使用强引用来进行操作,并在使用完毕后删除临时创建的强引用(如果是全局引用)或等待局部引用自动释放。 注意:提升为强引用的操作(NewLocalRef/NewGlobalRef)本身会返回一个新的引用,即使弱引用指向的对象还没有被回收,我们也不能直接使用弱引用(因为垃圾回收器可能在之后立即回收它,即使我们刚检查过)。所以,我们使用强引用来确保在操作期间对象不会被回收。 另外,根据引用[1]中提到的显式销毁方法,如果我们的弱全局引用是作为某个本地对等体(native peer)的一部分,那么我们也应该在Java对等体被回收时(在finalize方法中)调用一个本地方法来清理弱全局引用(即调用DeleteWeakGlobalRef)。否则,即使Java对象被回收,弱全局引用仍然占用一个引用槽位,虽然它不会阻止对象回收,但过多的未删除弱全局引用也会浪费资源。 示例:清理弱全局引用 在Java类中,添加finalize方法(或者更好的方式,使用Cleaner或PhantomReference,但JNI中常用finalize): ```java public class MyJavaClass { // 指向本地对等体的指针(通常是一个long型变量) private long nativePtr; // ... @Override protected void finalize() throws Throwable { try { // 调用本地方法清理本地对等体,包括弱全局引用 nativeDestroy(nativePtr); } finally { super.finalize(); } } private static native void nativeDestroy(long nativePtr); } ``` 在本地代码中: ```c++ JNIEXPORT void JNICALL Java_com_example_MyJavaClass_nativeDestroy(JNIEnv* env, jclass, jlong nativePtr) { MyPeer* peer = reinterpret_cast<MyPeer*>(nativePtr); if (peer) { // 清理弱全局引用 if (peer->weakRef) { env->DeleteWeakGlobalRef(peer->weakRef); peer->weakRef = nullptr; } // 删除本地对等体 delete peer; } } ``` 总结:解决“JNI ERROR attempt to use stale WeakGlobal reference”的关键在于: 1. 在使用弱引用前检查其有效性(通过提升为强引用)。 2. 及时清理不再需要的弱全局引用(避免无用的引用残留)。 如果按照上述方法修复,应该可以避免这个错误。</think>### 解决 JNI 错误:`attempt to use stale WeakGlobal reference` 当在 JNI 中使用弱全局引用(`WeakGlobalRef`)时,出现 **`JNI ERROR attempt to use stale WeakGlobal reference`** 错误,表示你尝试访问一个已被垃圾回收的 Java 对象。以下是根本原因和完整解决方案: --- ### 错误原因分析 1. **弱引用特性** `WeakGlobalRef` 不会阻止 Java 对象被垃圾回收(与 `GlobalRef` 不同)。当 Java 对象被回收后,弱引用会变成 **"过时引用"**(stale reference)。 2. **未检查引用有效性** 直接使用弱引用前未检查对象是否已被回收: ```cpp // 错误示例:未检查直接使用 jobject obj = g_weakRef; // g_weakRef 可能已被回收 env->CallVoidMethod(obj, ...); // 触发 JNI ERROR ``` 3. **生命周期管理缺失** 未在 Java 对象被回收时及时清理弱引用(导致持有无效指针)。 --- ### 完整解决方案 #### 1. **检查引用有效性** 使用弱引用前**必须**检查对象是否存活: ```cpp JNIEnv* env = ...; jobject strongRef = env->NewLocalRef(g_weakRef); // 尝试提升为强引用 if (strongRef != nullptr) { // 对象存活,安全使用 env->CallVoidMethod(strongRef, ...); env->DeleteLocalRef(strongRef); // 及时释放 } else { // 对象已被回收,清理弱引用 env->DeleteWeakGlobalRef(g_weakRef); g_weakRef = nullptr; } ``` #### 2. **正确管理弱引用生命周期** **方案A:Java 端触发清理**(推荐) 在 Java 对象的 `finalize()` 中调用 Native 清理函数: ```java public class MyJavaClass { private long nativePtr; // 指向 Native 对象的指针 protected void finalize() throws Throwable { try { nativeCleanup(nativePtr); // 调用 JNI 清理函数 } finally { super.finalize(); } } private static native void nativeCleanup(long nativePtr); } ``` **方案B:Native 端显式清理** 创建显式的销毁函数: ```cpp // C++ 类封装 class JavaObjectWrapper { public: JavaObjectWrapper(JNIEnv* env, jobject obj) { weakRef = env->NewWeakGlobalRef(obj); // 创建弱引用 } void destroy(JNIEnv* env) { // 显式清理函数 if (weakRef != nullptr) { env->DeleteWeakGlobalRef(weakRef); weakRef = nullptr; } } jobject weakRef = nullptr; }; // JNI 导出函数 JNIEXPORT void JNICALL Java_com_example_MyClass_destroyWrapper(JNIEnv* env, jobject, jlong ptr) { auto* wrapper = reinterpret_cast<JavaObjectWrapper*>(ptr); wrapper->destroy(env); // 调用显式清理 delete wrapper; } ``` #### 3. **关键注意事项** 1. **禁止缓存 `JNIEnv`** `JNIEnv` 是线程局部的,每次使用需重新获取(通过 `AttachCurrentThread`)[^5]。 2. **弱引用使用原则** ```cpp // 正确流程: jobject weakRef = env->NewWeakGlobalRef(javaObj); // 创建 // ... jobject tmp = env->NewLocalRef(weakRef); // 使用前提升 if (tmp) { /* 安全使用 */ } // 检查有效性 env->DeleteLocalRef(tmp); // 及时释放 // ... env->DeleteWeakGlobalRef(weakRef); // 最终清理 ``` 3. **线程安全** 在跨线程访问弱引用时,必须通过 `AttachCurrentThread` 附加到 JVM: ```cpp void worker_thread(JavaVM* jvm) { JNIEnv* env; jvm->AttachCurrentThread(&env, nullptr); // 此处安全访问弱引用 jobject tmp = env->NewLocalRef(g_weakRef); if (tmp) { /* ... */ } jvm->DetachCurrentThread(); } ``` --- ### 最佳实践总结 | 操作 | 正确做法 | 错误做法 | |---------------------|---------------------------------------------|----------------------------| | **创建弱引用** | `env->NewWeakGlobalRef()` | 直接赋值 `jobject` | | **使用弱引用** | 先提升为 `LocalRef` 并检查 `nullptr` | 直接调用方法 | | **清理弱引用** | Java `finalize()` + Native 显式清理 | 依赖析构函数(无 `JNIEnv`) | | **跨线程访问** | 先 `AttachCurrentThread` 附加线程 | 跨线程共享 `JNIEnv` | 遵循以上方案可彻底解决 `stale WeakGlobal reference` 错误,并避免 JVM 崩溃[^1][^5]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值