You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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**)&currentEnv, 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(&currentEnv, &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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 00:30:08