网站首页 > 技术文章 正文
一、背景
近期在项目中遇到一个困扰了2天的问题,虽然知道问题的原因,但始终不明所以,解决的方式也是囫囵吞枣。问题的起因是,项目中封装了一个统一的JNI 回调函数,将Native层C++ 调用Java 的函数统一接管,做过JNI 的都知道,Native调用Java 回调函数时需要考虑跨线程的情况,即在JNI 线程调用和不在JNI 线程调用的情况(有人喜欢把这类线程叫作子线程)。在JNI 线程时操作起来比较简单,直接调用即可。但不在JNI 线程时需要先对相关对象附在JNI 线程中,否则会闪退。
本文的问题在调用时已经考虑了线程的附加与分离,但依然报下述crash :
//(局部引用非法)
java_vm_ext.cc:598] JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid local reference: 0x7a38ba2035 (reference outside the table: 0x7a38ba2035)
java_vm_ext.cc:598] JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid local reference: 0x7a38ba2035 (reference outside the table: 0x7a38ba2035)
java_vm_ext.cc:598] in call to GetObjectClass
2025-08-04 10:18:43.917 32203-7932 n.collaboration com.tran.collaboration A runtime.cc:708] Runtime aborting...
runtime.cc:708] Dumping all threads without mutator lock held
runtime.cc:708] All threads:
runtime.cc:708] DALVIK THREADS (26):
runtime.cc:708] "main" prio=10 tid=1 Native
runtime.cc:708] | group="" sCount=1 ucsCount=0 flags=1 obj=0x72adc9e8 self=0xb40000784b84df50
runtime.cc:708] | sysTid=32203 nice=-10 cgrp=top-app sched=0/0 handle=0x7a3a0844f8
runtime.cc:708] | state=S schedstat=( 807140063 37525466 241 ) utm=68 stm=12 core=6 HZ=100
二、原因分析
分析这个问题之前,先将问题涉及的代码贴出来,这样好讲。
统一的回调函数:
JavaObjCtx.h
//
// Created by Rambo.liu on 2025/8/1.
// java object 管理器
#ifndef INPUTSHARE_JAVAOBJCTX_H
#define INPUTSHARE_JAVAOBJCTX_H
#include <jni.h>
class JavaObjCtx {
public:
JNIEnv *env;
//类对象
jclass clazz;
//构造器
jmethodID constructor;
//实例对象
jobject instanceObj;
//方法ID
jmethodID methodId;
JavaVM *jvm;
/**
* java 对象
* @param _jvm
* @param _env
* @param clzz 类 需要包名 如:com/tran/share/input/Injection
* @param methodName 方法名
* @param methodSign 方法签名
*/
JavaObjCtx(JavaVM *_jvm,JNIEnv *_env,const char* clzz,const char* methodName,const char* methodSign);
~JavaObjCtx();
/**
* 调用函数
* @param obj 实例对象,需要注意跨线程调用,
* 注意:obj 在跨线程调用时,当第一次在 JNI 线程调用时工作正常,但第二次在非 JNI 线程调用时,原始局部引用 jobject 已经失效。
* 解决方案为:在首次获取 Java 对象时,直接转换为全局引用
* 第二次调用时 obj 打印不为空但实际已失效,是 JNI 局部引用机制的一个典型陷阱
* 当 JNI 方法返回时,整个局部引用表会被自动清空,但 C++ 变量 obj 仍保留原地址值
* 类似「野指针」现象:指针地址非 NULL,但指向的内容已无效
* @param methodID
* @param ...
*/
void callVoidMethod(jobject obj, jmethodID methodID, ...);
bool isReferenceValid(JNIEnv* env, jobject obj);
};
#endif //INPUTSHARE_JAVAOBJCTX_H
JavaObjCtx.cpp
//
// Created by Rambo.liu on 2025/8/1.
//
#include "JavaObjCtx.h"
#include "Log.h"
JavaObjCtx::JavaObjCtx(JavaVM *_jvm,JNIEnv *_env, const char *clzz, const char *methodName,
const char *methodSign) {
LOGI("class = %s methodId = %s methodSign = %s", clzz, methodName, methodSign);
env = _env;
jvm = _jvm;
//先找类对象
jclass injectionCls = env->FindClass(clzz);
if (injectionCls == nullptr) {
LOGE("找不到 %s 类对象", clzz);
return;
}
//找到类对象,先初始化,获取类对象实例,使用默认的构造方法
jmethodID _constructor = env->GetMethodID(injectionCls, "<init>", "()V");
if (nullptr == _constructor) {
LOGW("can't constructor injectionCls");
return;
}
//构造injectionCls 实例对象
jobject injectionClsObject = env->NewObject(injectionCls, _constructor);
if (nullptr == injectionClsObject) {
LOGW("can't new injectionClsObject");
env->DeleteLocalRef(injectionCls);
return;
}
//查找要调用的方法
jmethodID _methodId = env->GetMethodID(injectionCls, methodName,
methodSign);
if (_methodId == nullptr) {
LOGV("onStatus 未找到,直接释放类对象与实例对象");
env->DeleteLocalRef(injectionCls);
env->DeleteLocalRef(injectionClsObject);
return;
}
clazz = injectionCls;
instanceObj = injectionClsObject;
constructor = _constructor;
methodId = _methodId;
}
JavaObjCtx::~JavaObjCtx() {
LOGI("JavaObjCtx::~JavaObjCtx()析构");
if (env) {
if (clazz) {
env->DeleteLocalRef(clazz);
}
}
}
void JavaObjCtx::callVoidMethod(jobject obj, jmethodID methodID, ...) {
bool attached = false;
if (!env) {
LOGE("JNIEnv is null!");
return;
}
if (!obj) {
LOGE("Java object is null!");
return;
}
// 检查线程是否已附加
if (jvm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED) {
if (jvm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
attached = true;
LOGI("运行在非主线程中,附加成功");
} else {
LOGE("此函数运行在非创建JNI线程中,附加JVM 失败!!");
return;
}
} else {
LOGI("函数已运行在创建JNI 线程中,无需附加");
}
LOGD("跨线程地址: %p", obj); // 地址相同
LOGD("是否有效: %d", isReferenceValid(env, obj)); // 必为false
va_list args;
va_start(args, methodID);
env->CallVoidMethodV(obj, methodID, args);
va_end(args);
// 检查调用是否产生异常
if (env->ExceptionCheck()) {
LOGD("Exception occurred while calling method");
env->ExceptionDescribe();
env->ExceptionClear();
}
if (attached) {
jvm->DetachCurrentThread();
}
}
bool JavaObjCtx::isReferenceValid(JNIEnv *env, jobject obj) {
if (!obj){
LOGI("obj 已经为空");
return false;
}
jclass objClass = env->GetObjectClass(obj); // 关键检查
if (env->ExceptionCheck() || !objClass) {
env->ExceptionClear();
LOGI("obj 已经为空");
return false;
}
return true;
}
出现问题的场景为:
- Socket 初始时针对Socket初始化各种失败的场景将结果回调给java 层做业务交互。这类场景都在JNI 线程中调用 callVoidMethod。调用的代码如下:
//回调函数
std::function<void(CONNECTED_STATUS status)> _connectCallBack;
//状态改变时 触发回调
void setStatus(CONNECTED_STATUS status) {
_status = status;
LOGI("80----------- status = %d",status);
if(_connectCallBack) {
_connectCallBack(status);
}
}
void start(std::string &ip, const int port) {
if (getStatus() == CONNECTED_STATUS::CONNECTING) {
LOGI("正在连接中...");
return;
} else if (getStatus() == CONNECTED_STATUS::CONNECTED) {
LOGI("已连接成功,无需要重复连接!!!");
return;
}
setStatus(CONNECTED_STATUS::CONNECTING);
_ip = ip;
_port = port;
// 1. 若已有线程在运行,先终止并清理
if (_connect_thread && _connect_thread->joinable()) {
_connect_thread->join(); // 等待旧线程结束
_connect_thread.reset(); // 释放线程资源
}
_is_connected = false;
_stop = true;
//第二次创建线程调用socket connect
std::future<bool> connectFuture = commit(
std::bind(&SocketClient::connectServer, this, ip, port));
_is_connected = connectFuture.get();
if (_is_connected) {
_conn_cond.notify_one();
std::packaged_task<bool()> task([this, ip, port]() {
LOGI("连接成功,服务器地址:%s 端口:%d", ip.c_str(), port);
_reconnect_count = 1;
_stop = false;
setStatus(CONNECTED_STATUS::CONNECTED);
initEventLoop();
read();
return _is_connected;
});
_connect_thread = std::make_unique<std::thread>(std::move(task));
}
}
- 第二次在Socket 连接服务器时调用,这次是开了一个线程来调用Socket 的connect,(非JNI 线程调用)callVoidMethod。(注 上述代码的红色的注释部分)
- 外部调用时两个调用对象为同一个,调用部分代码为:
std::shared_ptr<SocketClient> _socketClient;
std::unique_ptr<JavaObjCtx> _javaObjCtx;
extern "C"
JNIEXPORT void JNICALL
testSocket(JNIEnv *env, jclass thiz, jstring ip, jint port) {
jboolean isCopy;
const char * szIp = env->GetStringUTFChars(ip, &isCopy);
if(!_socketClient) {
_socketClient = std::make_shared<SocketClient>();
}
std::string s(szIp);
if(!_javaObjCtx) {
_javaObjCtx = std::make_unique<JavaObjCtx>(g_jvm,env,"com/tran/share/input/Injection","onStatus",
"(I)V");
}
//收到回调时由此统一向java 回吐
_socketClient->setConnectStatusCallBack([&env](CONNECTED_STATUS status)->void {
_javaObjCtx->callVoidMethod(_javaObjCtx->instanceObj, _javaObjCtx->methodId, status);
});
_socketClient->start(s , port);
env->ReleaseStringUTFChars(ip, szIp);
}
Java 层的回调函数:
package com.tran.share.input;
import android.util.Log;
import java.io.DataOutputStream;
/**
* author rambo.liu
* date 2025/7/15 11:04
* Version: 1.0
* Description: 事件注入器
*/
public class Injection {
private final static String TAG = "Injection";
static {
System.loadLibrary("inputShare");
}
public void onStatus(int code) {
Log.i(TAG,"58--------收到来自Native 的回调 code = "+code);
}
}
业务需求很简单,但问题却不小。在JNI 对象中明明考虑了跨线程的场景,而且对相关的对象都做判空处理,
callVoidMethod 中判空代码
void JavaObjCtx::callVoidMethod(jobject obj, jmethodID methodID, ...) {
bool attached = false;
//判空
if (!env) {
LOGE("JNIEnv is null!");
return;
}
//判空
if (!obj) {
LOGE("Java object is null!");
return;
}
// 检查线程是否已附加
if (jvm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED) {
if (jvm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
attached = true;
LOGI("运行在非主线程中,附加成功");
} else {
LOGE("此函数运行在非创建JNI线程中,附加JVM 失败!!");
return;
}
} else {
LOGI("函数已运行在创建JNI 线程中,无需附加");
}
va_list args;
va_start(args, methodID);
env->CallVoidMethodV(obj, methodID, args);
va_end(args);
// 检查调用是否产生异常
if (env->ExceptionCheck()) {
LOGD("Exception occurred while calling method");
env->ExceptionDescribe();
env->ExceptionClear();
}
if (attached) {
jvm->DetachCurrentThread();
}
}
为毛还报 "java_vm_ext.cc:598] JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid local reference: 0x7a38ba2035 (reference outside the table: 0x7a38ba2035) (局部引用非法)"
产生这个问题的根本原因:
当第一次在 JNI 线程调用时工作正常,但第二次在非 JNI 线程调用时,原始局部引用 jobject 已经失效。跨线程传递未保护的局部引用导致的。
- 第一次调用(JNI 线程)
- 局部引用 obj 有效,因为仍在原始 JNI 上下文生命周期内
- 第二次调用(非 JNI 线程)
- 局部引用 obj 已随第一次 JNI 调用结束被自动释放
- 直接使用会导致 invalid local reference 错误
这里还有一个问题需要搞清楚,不然没法理解:
既然已经失效了,按照常规理解,指针指向的内存应该也已经被释放了,callVoidMethod方法中判空应该会被阻断,但上述代码并非如此?何解?
第二次调用时callVoidMthod obj 打印不为空,其实是忽略了一个C/C++ 中的常见问题,即野指针问题。虽然指针指向的内存释放了,但指针并未指空,导致了该问题出现。这也是 JNI 局部引用机制的一个典型陷阱。以下是深度解析:
- 打印显示非空但实际无效
- C++ 层的 jobject 本质上是一个指针(类似 void*),打印时只是显示内存地址值
- 局部引用被释放后,指针地址不会自动置零,但 JVM 内部已将其标记为无效
- JNI 局部引用表机制
jobject obj = env->NewLocalRef(someObj); // 添加到当前线程的局部引用表
- 当 JNI 方法返回时,整个局部引用表会被自动清空,但 C++ 变量 obj 仍保留原地址值
- 类似「野指针」现象:指针地址非 NULL,但指向的内容已无效
为了验证上述分析,可以使用如下代码测试:
extern "C" JNIEXPORT void JNICALL
Java_com_example_Test_printRef(JNIEnv* env, jobject thiz, jobject javaObj) {
jobject localRef = env->NewLocalRef(javaObj);
LOGD("第一次地址: %p", localRef); // 输出类似 0x7a38ba2039
// 模拟跨线程传递(错误做法!)
std::thread([=]{
LOGD("第二次地址: %p", localRef); // 地址相同但已失效!
}).join();
}
输出结果:
第一次地址: 0x7a38ba2039
第二次地址: 0x7a38ba2039 // 相同地址但实际已失效
三、解决方案
既然局部引用已经 失效,但就转换为全局引用,将传递给callMethodId 方法的jobject 参数转换为实全局引用,调用的地方改为如下:
std::shared_ptr<SocketClient> _socketClient;
std::unique_ptr<JavaObjCtx> _javaObjCtx;
JavaVM *g_jvm = nullptr;
jobject g_globalObj = nullptr;
extern "C"
JNIEXPORT void JNICALL
testSocket(JNIEnv *env, jclass thiz, jstring ip, jint port) {
jboolean isCopy;
const char * szIp = env->GetStringUTFChars(ip, &isCopy);
if(!_socketClient) {
_socketClient = std::make_shared<SocketClient>();
}
std::string s(szIp);
if(!_javaObjCtx) {
_javaObjCtx = std::make_unique<JavaObjCtx>(g_jvm,env,"com/tran/share/input/Injection","onStatus",
"(I)V");
if (g_globalObj) env->DeleteGlobalRef(g_globalObj);
//全局引用
g_globalObj = env->NewGlobalRef(_javaObjCtx->instanceObj);
}
_socketClient->setConnectStatusCallBack([&env](CONNECTED_STATUS status)->void {
//jobject 改为全局引用
_javaObjCtx->callVoidMethod(g_globalObj, _javaObjCtx->methodId, status);
});
_socketClient->start(s , port);
env->ReleaseStringUTFChars(ip, szIp);
}
//不用的时候记得释放全局指针
JNIEXPORT void JNI_OnUnload(JavaVM *vm, void *reserved) {
LOGV("卸載---------如果有全局引用需要將其移除");
JNIEnv* env;
vm->GetEnv((void**)&env, JNI_VERSION_1_6);
if (g_globalObj) {
env->DeleteGlobalRef(g_globalObj);
g_globalObj = nullptr;
}
}
除了jobject 这类参数有此问题之外,还有一个对象在跨线程使用的过程中也容易忽视-- JNIEnv *env。在跨线程使用过程中需要注意以下几点:
- 线程安全
JNIEnv* 是线程私有对象,不能跨线程传递。如果在新线程中使用 env,必须先附加到 JVM 并获取当前线程的 env:按如下方式获取当前线程的JNIEnv 指针
// 在 C++ 新线程中获取 JNIEnv* 的正确方式
JNIEnv* env = nullptr;
// 假设 vm 是全局的 JavaVM* 指针(通过 JNI_OnLoad 获取)
int ret = vm->AttachCurrentThread(&env, nullptr);
if (ret != JNI_OK || env == nullptr) {
LOGD("Failed to attach thread");
return;
}
// 使用 env 前检查有效性
if (env->ExceptionCheck()) { ... }
// 线程结束时分离
vm->DetachCurrentThread();
- 生命周期:env 仅在当前 JNI 调用或附加的线程生命周期内有效,不要将其保存为全局变量长期使用。
在上述代码JavaObjCtx.h 中刚好把JNIEnv 当作成员变量了:
//
// Created by Rambo.liu on 2025/8/1.
// java object 管理器
#ifndef INPUTSHARE_JAVAOBJCTX_H
#define INPUTSHARE_JAVAOBJCTX_H
#include <jni.h>
class JavaObjCtx {
public:
//成员变量
JNIEnv *env;
//类对象
jclass clazz;
//构造器
jmethodID constructor;
//实例对象
jobject instanceObj;
//方法ID
jmethodID methodId;
JavaVM *jvm;
};
#endif //INPUTSHARE_JAVAOBJCTX_H
源文件JavaObjCtx.cpp 中构造器中给JNIEnv* env赋值:
JavaObjCtx::JavaObjCtx(JavaVM *_jvm,JNIEnv *_env, const char *clzz, const char *methodName,
const char *methodSign) {
LOGI("class = %s methodId = %s methodSign = %s", clzz, methodName, methodSign);
//给成员变量赋值
env = _env;
jvm = _jvm;
//先找类对象
......
}
这在跨线程调用函数:
void JavaObjCtx::callVoidMethod(jobject obj, jmethodID methodID, ...) {
bool attached = false;
if (!env) {
return;
}
if (!obj) {
return;
}
// 检查线程是否已附加
if (jvm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED) {
if (jvm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
attached = true;
//LOGI("运行在非主线程中,附加成功");
} else {
//LOGE("此函数运行在非创建JNI线程中,附加JVM 失败!!");
return;
}
} else {
//LOGI("函数已运行在创建JNI 线程中,无需附加");
}
va_list args;
va_start(args, methodID);
env->CallVoidMethodV(obj, methodID, args);
va_end(args);
// 检查调用是否产生异常
if (env->ExceptionCheck()) {
LOGD("Exception occurred while calling method");
env->ExceptionDescribe();
env->ExceptionClear();
}
if (attached) {
jvm->DetachCurrentThread();
}
}
上述代码中使用的 env 由于是类的成员变量(而非局部变量),但 JNIEnv* 是线程私有的(每个线程有自己独立的 JNIEnv*),不能作为类成员跨线程共享。理由如下:
- 当在新线程中调用此函数时,成员变量 env 可能仍是其他线程的无效值(甚至 nullptr)。
- 即使通过 GetEnv 或 AttachCurrentThread 获取了当前线程的 env,若未正确更新成员变量,后续使用的仍是旧的无效 env。
如果在跨线程调用时,env 为成员变量同时又没有随线程更新,就会出现下面的crash 情况:
JNI 中 2025-08-04 19:15:16.227 8925-10816 libc com.tran.collaboration A Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 10816 (Thread-2), pid 8925 (n.collaboration)
Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 是 JNI 开发中常见的内存访问错误,本质是 C/C++ 代码尝试访问无效的内存地址(这里是 0x0,即空指针 NULL),导致操作系统触发段错误(Segmentation Fault)。
错误核心原因
SIGSEGV 是 Linux 系统的 “段错误” 信号,SEGV_MAPERR 表示程序尝试访问的内存地址未被映射到合法的内存空间,而 fault addr 0x0 明确指出:代码中存在对空指针(NULL)的解引用操作(比如访问 NULL->field 或 *(NULL))。
修复方案
核心原则:JNIEnv* 应作为局部变量使用,每次通过 JavaVM* 获取当前线程的有效实例,并严格检查所有指针的有效性。完整方案见文首的JavaObjCtx.h 代码
四、小结
场景 | 正确做法 |
需要跨线程使用 | 必须用 NewGlobalRef 转换 |
临时局部引用 | 确保不跨越 JNI 方法调用 |
调试检查 | 用 GetObjectClass 检测有效性 |
技术类比
JNI 概念 | 类似 C++ 概念 | 关键区别 |
局部引用 | 栈指针 | 自动管理生命周期 |
全局引用 | new 分配的对象 | 需手动释放 |
弱全局引用 | weak_ptr | 可能被 GC 回收 |
绝对禁忌
// 错误!跨线程传递局部引用
void dangerousCall(jobject obj) {
std::thread([=]{
// 即使 obj 地址非空,实际已失效
someJNICall(obj);
}).detach();
}
记住:在 JNI 中,指针非空 ≠ 引用有效!必须通过 JVM 机制验证。
猜你喜欢
- 2025-09-12 C# Token 浅析_.net core token
- 2025-09-12 Spring源码|Spring实例Bean的方法
- 2025-09-12 Metasploitable2笔记(漏洞利用与加固)
- 2025-09-12 .NET Core 中推荐使用的10大优秀库,你用到过几个?
- 2025-09-12 C# RulesEngine 规则引擎:从入门到看懵
- 2025-09-12 World Insights: Japan's plot to discharge Fukushima nuclear-contaminated wastewater into sea
- 2025-09-12 用 Python 守护你的 API:从入门到实践的安全监测指南
- 2025-09-12 Android Framework 输入子系统 (10)Input命令解读
- 2025-09-12 《应急响应流程;日志分析技巧》_应急响应怎么写
- 2025-07-03 探索 Swift 中的 MVC-N 模式(mvc模式是什么)
- 最近发表
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- apt-getinstall-y (100)
- node_modules怎么生成 (87)
- chromepost (71)
- flexdirection (73)
- c++int转char (80)
- mysqlany_value (79)
- static函数和普通函数 (84)
- el-date-picker开始日期早于结束日期 (76)
- js判断是否是json字符串 (75)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)