[Android] JNI ERROR (app bug): attempt to use stale Local 0xHHHHHHHH*

Android JNI时偶现attempt to use stale Local问题,同时报错log还给出提示说这个问题是app bug,立马坐不住了,必须看看这是不是诬告~
先解释一下报错是什么意思:

JNI ERROR (app bug): attempt to use stale Local 0xHHHHHHHH
JNI 错误(应用层缺陷):试图使用一个过期的局部引用0xHHHHHHHH

从Android 4开始JNI中引入了局部引用(Local Ref)和全局引用(Global Ref)的概念,JNI中的引用(Reference)是指在native代码中引用Java代码中的对象的句柄。局部引用(Local Ref)是用于在当前线程下持有的引用就在当前线程下使用,其他线程不可使用。全局引用(Global Ref)是用于在当前线程下持有的引用公开给本进程中所有线程使用,即可把局部引用转换为全局引用来使用。

问题代码

01 | #include <jni.h>
02 |
03 | static jclass gInputStream_Clazz;
04 | static jmethod gInputStream_Method_reset;
05 | static jmethod gInputStream_Method_mark;
06 | 
07 | void initJavaStream(JNIEnv* env) {
08 |     static bool gIsInitedJavaStream;
09 |     if (!gIsInitedJavaStream) {
10 |         gInputStream_Clazz = env->FindClass("java/io/InputStream");
11 |         gInputStream_Clazz = (jclass) env->NewGlobalRef(gInputStream_Clazz);
12 |         gInputStream_Method_reset = env->GetMethodID(gInputStream_Clazz, "reset", "()V");
13 |         gInputStream_Method_mark = env->GetMethodID(gInputStream_Clazz, "mark", "(I)V");
14 |         gIsInitedJavaStream = true;
15 |     }
16 | }
17 | 
18 | void doSomethingWithJavaStream(JNIEnv* env) {
19 |     gInputStream_Clazz .....
20 | }

报错堆栈

Build fingerprint: '...................................'
Revision: '0'
ABI: 'arm64'
Timestamp: 2021-03-11 11:40:17+0800
pid: 23537, tid: 29233, name: XXXThead  >>> com.example.demo <<<
uid: 10076
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'JNI ERROR (app bug): attempt to use stale Local 0x29 (should be 0x25)'
    x0  0000000000000000  x1  0000000000007231  x2  0000000000000006  x3  00000075946fe4d0
    x4  00000076b4cda000  x5  00000076b4cda000  x6  00000076b4cda000  x7  000000000121b87e
    x8  00000000000000f0  x9  0008c5ca4be56366  x10 0000000000000000  x11 ffffffc0fffffbdf
    x12 0000000000000001  x13 ffffffff9fb73d4c  x14 000000000bd92a3f  x15 ffffffffffffffff
    x16 00000076b13e87e0  x17 00000076b13c7da0  x18 0000007541580000  x19 0000000000005bf1
    x20 0000000000007231  x21 00000000ffffffff  x22 b400007593f54400  x23 0000000000000000
    x24 000000762b6b8000  x25 000000762b0be360  x26 000000762af3286c  x27 000000762b6ba000
    x28 000000762b6bb000  x29 00000075946fe550
    lr  00000076b137b460  sp  00000075946fe4b0  pc  00000076b137b48c  pst 0000000000000000

backtrace:
      #00 pc 000000000007848c  /apex/com.android.runtime/lib64/bionic/libc.so (abort+164)
      #01 pc 000000000055db6c  /apex/com.android.art/lib64/libart.so (art::Runtime::Abort(char const*)+2308)
      #02 pc 0000000000013988  /system/lib64/libbase.so (android::base::SetAborter(std::__1::function<void (char const*)>&&)::$_3::__invoke(char const*)+76)
      #03 pc 0000000000012fb4  /system/lib64/libbase.so (android::base::LogMessage::~LogMessage()+320) (BuildId: a09a41f1c2370328c811e735cdaa2860)
      #04 pc 00000000002f96ac  /apex/com.android.art/lib64/libart.so (art::IndirectReferenceTable::AbortIfNoCheckJNI(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)+224)
      #05 pc 000000000038c54c  /apex/com.android.art/lib64/libart.so (art::IndirectReferenceTable::GetChecked(void*) const+444)
      #06 pc 00000000005b2fb8  /apex/com.android.art/lib64/libart.so (art::Thread::DecodeJObject(_jobject*) const+96)
      #07 pc 0000000000391a08  /apex/com.android.art/lib64/libart.so (art::FindMethodJNI(art::ScopedObjectAccess const&, _jclass*, char const*, char const*, bool)+68)
      #08 pc 000000000039e87c  /apex/com.android.art/lib64/libart.so (art::JNI<false>::GetMethodID(_JNIEnv*, _jclass*, char const*, char const*)+660)
      #09 pc 0000000000009ac0  /my_stock/app/Demo/Demo.apk!libnative.so (offset 0x6671000) (initJavaStream(_JNIEnv*)+316)
      .................

原因

由于FindClass返回的gInputStream_Clazz为局部引用(LocalRef),而接下来gInputStream_Clazz会在doSomethingWithJavaStream中被任意线程调用,因此需要把gInputStream_Clazz转换为全局引用(GlobalRef)。

但当initJavaStream被两条线程一前一后紧接着调用时,问题就出现了:

Thread1 (line09) -> 判断gIsInitedJavaStream为false,进入if
Thread1 (line10) -> FindClass返回LocalRef给gInputStream_Clazz
Thread2 (line09) -> 判断gIsInitedJavaStream为false,进入if
Thread1 (line11) -> 通过NewGlobal将gInputStream_Clazz从LocalRef转为GlobalRef
Thread1 (line12) -> 使用有效的gInputStream_Clazz引用来获取InputStream的reset方法的句柄
Thread2 (line10) -> FindClass返回LocalRef给gInputStream_Clazz

......此时可能出现GC,JVM移动了gInputStream_Clazz指向的Java对象的内存位置......

Thread1 (line13) -> 使用有效的gInputStream_Clazz引用来获取InputStream的mark方法的句柄 【崩溃!gInputStream_Clazz无效!】

解决方法

01 | #include <jni.h>
02 | #include <pthread.h>
03 | 
04 | static jclass gInputStream_Clazz;
05 | static jmethod gInputStream_Method_reset;
06 | static jmethod gInputStream_Method_mark;
07 | 
08 | static pthread_mutex_t mutex;
09 | 
10 | void init() {
11 |     pthread_mutex_init(&mutex, NULL);
12 | }
13 | 
14 | void initJavaStream(JNIEnv* env) {
15 |     static bool gIsInitedJavaStream;
16 | 
17 |     pthread_mutex_lock(&mutex);
18 |     if (!gIsInitedJavaStream) {
19 |         jclass localClazz = env->FindClass("java/io/InputStream");
20 |         gInputStream_Clazz = (jclass) env->NewGlobalRef(localClazz);
21 |         gInputStream_Method_reset = env->GetMethodID(gInputStream_Clazz, "reset", "()V");
22 |         gInputStream_Method_mark = env->GetMethodID(gInputStream_Clazz, "mark", "(I)V");
23 |         gIsInitedJavaStream = true;
24 |     }
25 |     pthread_mutex_unlock(&mutex);
26 | }
27 | 
28 | void doSomethingWithJavaStream(JNIEnv* env) {
29 |     gInputStream_Clazz .....
30 | }
31 | 
32 | void deinit() {
33 |     pthread_mutex_destroy(&mutex);
34 | }

无论在Java层保证还是在native上保证都可以,只要能够保证initJavaStream的过程是线程安全的,此问题即可得到解决。
同时,从编码习惯上也应该规避一下这个问题,第19行FindClass返回的结果此时应该使用一个局部变量来持有,而不应该偷懒地直接赋值给全局可访问的static变量持有,这个编码习惯在一定程度上可以规避这个崩溃,虽然同样会造成Ref泄露,但这个习惯依然是一个可以防坑的好习惯。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容