GoLang(Cgin服务)结合CPP JNI调用Java库遇段错误问题
问题:CGO+JNI调用Java库请求阶段触发段错误
Java库实现
提供两个静态方法:
initCache:调用API并缓存响应(缓存逻辑复杂,暂不重构)search:查询缓存并返回结果
public static void initCache(String url, String ttl) throws Exception { try { // 缓存初始化逻辑 // 曾尝试用while(true)维持线程存活,日志输出正常 } catch (Exception e) { e.printStackTrace(); } } public static String search(String txt, String id) throws Exception { try { return txt; // 简化实现,仅返回输入参数 } catch (Exception e) { e.printStackTrace(); } }
Go服务场景
- 应用启动时调用
initCache,执行完全正常 - HTTP请求处理阶段调用
search,触发段错误
C++桥接层代码
#include <chrono> #include <cstdlib> #include <iostream> #include <mutex> #include <thread> #include <jni.h> #include "librarycache.hpp" JNIEnv *javaEnv; jclass libraryClass_g; void initLibrary() { JavaVM *jvm; JavaVMInitArgs vm_args; JavaVMOption *options = new JavaVMOption[1]; options[0].optionString = "-Djava.class.path=onebigjarfile"; vm_args.version = JNI_VERSION_1_8; vm_args.nOptions = 1; vm_args.options = options; vm_args.ignoreUnrecognized = false; jint rc = JNI_CreateJavaVM(&jvm, (void **)&javaEnv, &vm_args); delete options; if (rc != JNI_OK) { exit(EXIT_FAILURE); } jclass libraryClass = javaEnv->FindClass("com/cache/boo/CacheManager"); libraryClass_g = (jclass)javaEnv->NewGlobalRef(libraryClass); javaEnv->DeleteLocalRef(libraryClass); jmethodID initCacheMid = javaEnv->GetStaticMethodID(libraryClass_g, "initCache", "(Ljava/lang/String;Ljava/lang/String;)V"); jstring jstr1 = javaEnv->NewStringUTF("http://urltoapi"); jstring jstr2 = javaEnv->NewStringUTF("60000"); javaEnv->CallStaticVoidMethod(libraryClass_g, initCacheMid, jstr1, jstr2); jmethodID searchFunction = javaEnv->GetStaticMethodID(libraryClass_g, "search", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); // 此处执行正常 jstring jstr3 = javaEnv->NewStringUTF("Pasu"); jstring jstr4 = javaEnv->NewStringUTF("Pathi"); javaEnv->CallStaticObjectMethod(libraryClass_g, searchFunction, jstr3, jstr4); std::cout << "Library initialized" << std::endl; } void search() { try { jstring jstr3 = javaEnv->NewStringUTF("Pasu"); jstring jstr4 = javaEnv->NewStringUTF("Pathi"); jmethodID searchFunction = javaEnv->GetStaticMethodID(libraryClass_g, "search", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); // 触发段错误 std::cout << "searchFunction success>>>>" << std::endl; // javaEnv->CallStaticObjectMethod(libraryClass_g, searchFunction, jstr3, jstr4); } catch (...) { std::cout << "Exception" << std::endl; } }
核心问题分析
JNIEnv是线程绑定的对象,不能跨线程复用:
initLibrary在Go启动主线程执行,获取的javaEnv属于该线程- Go的HTTP请求由goroutine处理,goroutine会绑定到不同的系统线程,此时直接使用全局的
javaEnv属于跨线程非法访问,JNI调用触发段错误 - 原代码未保存全局JavaVM指针,无法在其他线程获取合法的JNIEnv实例
解决方案
1. 修改全局变量,保存JavaVM指针
替换原来的全局JNIEnv,改为保存JavaVM实例:
JavaVM *jvm_g = nullptr; // 全局保存JVM指针,线程安全 jclass libraryClass_g;
2. 初始化时保存JVM指针
修改initLibrary中创建JVM的逻辑:
jint rc = JNI_CreateJavaVM(&jvm_g, (void **)&javaEnv, &vm_args); // 将jvm指针保存到全局变量
3. 重写search函数,动态获取当前线程的JNIEnv
确保每次调用时,当前线程已附加到JVM并获取合法的Env:
void search() { JNIEnv *currentEnv = nullptr; jint envStatus = jvm_g->GetEnv((void**)¤tEnv, JNI_VERSION_1_8); bool needDetach = false; // 线程未附加到JVM,执行附加操作 if (envStatus == JNI_EDETACHED) { JavaVMAttachArgs attachArgs{}; attachArgs.version = JNI_VERSION_1_8; attachArgs.name = nullptr; // 可自定义线程名 attachArgs.group = nullptr; if (jvm_g->AttachCurrentThread(¤tEnv, &attachArgs) != JNI_OK) { std::cerr << "Failed to attach thread to JVM" << std::endl; return; } needDetach = true; } // 获取Env失败,直接返回 else if (envStatus != JNI_OK) { std::cerr << "Failed to get JNIEnv" << std::endl; return; } try { // 创建JNI字符串局部引用 jstring jstr3 = currentEnv->NewStringUTF("Pasu"); jstring jstr4 = currentEnv->NewStringUTF("Pathi"); // 获取方法ID(此时使用当前线程合法的Env,不会触发段错误) jmethodID searchFunction = currentEnv->GetStaticMethodID(libraryClass_g, "search", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); if (!searchFunction) { std::cerr << "Failed to get search method ID" << std::endl; goto cleanup; } std::cout << "searchFunction success>>>>" << std::endl; // 调用Java方法并处理返回值 jstring resultStr = (jstring)currentEnv->CallStaticObjectMethod(libraryClass_g, searchFunction, jstr3, jstr4); if (resultStr) { const char* cResult = currentEnv->GetStringUTFChars(resultStr, nullptr); // 此处可将结果返回给Go层 std::cout << "Search result: " << cResult << std::endl; currentEnv->ReleaseStringUTFChars(resultStr, cResult); currentEnv->DeleteLocalRef(resultStr); } cleanup: // 释放局部引用,避免JNI内存泄漏 currentEnv->DeleteLocalRef(jstr3); currentEnv->DeleteLocalRef(jstr4); } catch (...) { std::cerr << "JNI call exception" << std::endl; } // 按需分离线程(临时线程建议分离,长期线程可保留附加状态) if (needDetach) { jvm_g->DetachCurrentThread(); } }
关键注意事项
- 永远不要跨线程复用JNIEnv,每个线程必须通过JVM获取自己的Env实例
- 全局的
jclass必须是全局引用(原代码已通过NewGlobalRef创建,符合要求) - 及时释放JNI局部引用(
DeleteLocalRef),避免JNI内存泄漏 - Go的goroutine可能频繁切换系统线程,因此每次JNI调用都要检查线程附加状态
内容的提问来源于stack exchange,提问作者Pasupathi Rajamanickam
相关产品推荐
相关产品推荐

