java 参数内存释放_JNI创建变量和释放变量

本文详细讲解了JNI中数据从Java到C和C到Java的传递技巧,包括基本数据类型和引用数据类型的处理,以及必要的释放机制。特别强调了内存管理规则,如本地引用与全局引用的区别,以及弱全局引用的使用。

jni中的数据传递就两种:c层传到java层;java层传到c层。

1 当数据从java传递到c

1.1 传递基本数据类型

在Java层定义jni代码:

public static native void inputInt(int intData);

c层实现:

JNIEXPORT void JNICALL

Java_com_hwilliam_jnilearncmake_NDKTools_inputInt(JNIEnv *env, jclass clazz, jint int_data) {

int data = int_data;

LOGD("input int data from java = %d", data);

}

由于jint是

typedef int32_t  jint;     /* signed 32 bits */

typedef __int32_t     int32_t;

typedef int __int32_t;

所以,jint就是int类型,所以直接用int类型接收就可以了。

其他的基本数据类型都是这样的,他们的映射关系定义在:

//  jni.h

/* Primitive types that match up with Java equivalents. */

typedef uint8_t  jboolean; /* unsigned 8 bits */

typedef int8_t   jbyte;    /* signed 8 bits */

typedef uint16_t jchar;    /* unsigned 16 bits */

typedef int16_t  jshort;   /* signed 16 bits */

typedef int32_t  jint;     /* signed 32 bits */

typedef int64_t  jlong;    /* signed 64 bits */

typedef float    jfloat;   /* 32-bit IEEE 754 */

typedef double   jdouble;  /* 64-bit IEEE 754 */

1.2 传递引用数据类型

在java层定义jni方法:

public static native void inputString(String stringData);

在c层定义实现:

JNIEXPORT void JNICALL

Java_com_hwilliam_jnilearncmake_NDKTools_inputString(JNIEnv *env, jclass clazz,

jstring string_data) {

//获取java中的string_data,转换成c中的字符串

const char *string = (*env)->GetStringUTFChars(env, string_data, NULL);

LOGD("input string data from java = %s", string);

//释放java中的string_data的引用。

(*env)->ReleaseStringUTFChars(env, string_data, string);

}

1.3 必要的释放

注意,从java传递基本数据类型的时候,是不需要释放引用的,因为基本数据类型在java层并不会导致内存泄漏。而对象的引用,才会导致内存泄漏。

当从java层传递一个引用数据类型(即一个java对象)到c层的时候,这个时候会把该对象的引用暴露给c层,让c层处理,即调用相应的GetXXX方法,例如传递String对象的时候,c层要调用jni函数来处理:(*env)->GetStringUTFChars,那么当处理完之后,必须手动释放调用c层对java层的该对象的引用,即调用对应的(*env)->ReleaseXXX函数。

即从java传递对象到c的时候,(*env)->GetXXX和(*env)->ReleaseXXX必须是成对出现的,否则就会造成内存泄漏。

2 当数据从c传递到java

从c层传递数据到java层一般涉及到两种方式:

java通过有返回值得jni方法调用进入到c层,然后通过该jni方法的返回值,c层返回数据到java层。数据是以同步调用的形式返回返回给java层的。

从c层主动调用java层的静态方法或者实例方法,以异步回调的方式将数据返回给java层。

2.1 传递基本数据类型

同步返回

java定义Jni方法

public static native int getIntFromCSync();

c层实现

JNIEXPORT jint JNICALL

Java_com_hwilliam_jnilearncmake_NDKTools_getIntFromCSync(JNIEnv *env, jclass clazz) {

int data = 100;

jint data2Java = data;

return data2Java;

}

因为jint其实就是int型,所以可以直接用int来接收,并直接返回给Java。

异步回调

Java层:定义一个jni方法,用于发起异步回调,然后定义一个java层的回调方法,这里的回调的参数是基本类型int。

public static native void getIntFromCAsync();

public static void onGetIntFromC(int dataFromC) {

LogUtils.d(dataFromC);

}

c层:实现jni方法,并通过CallXXXMethod()方法来回调java层的方法。

JNIEXPORT void JNICALL

Java_com_hwilliam_jnilearncmake_NDKTools_getIntFromCAsync(JNIEnv *env, jclass clazz) {

jint data2Java = 200;

jmethodID onGetIntFromC = (*env)->GetStaticMethodID(env, clazz, "onGetIntFromC", "(I)V");

(*env)->CallStaticVoidMethod(env, clazz, onGetIntFromC, data2Java);

}

2.2 传递引用数据类型

同步返回

public static native String getStringFromCSync();

JNIEXPORT jstring JNICALL

Java_com_hwilliam_jnilearncmake_NDKTools_getStringFromCSync(JNIEnv *env, jclass clazz) {

char *string = "abcdefg";

jstring result = (*env)->NewStringUTF(env, string);

return result;

}

异步回调

public static native void getStringFromCAsync();

public static void onGetStringFromC(String dataFromC) {

LogUtils.d(dataFromC);

}

JNIEXPORT void JNICALL

Java_com_hwilliam_jnilearncmake_NDKTools_getStringFromCAsync(JNIEnv *env, jclass clazz) {

//获取字符串

char *string = "123456789";

jstring result = (*env)->NewStringUTF(env, string);

//获取要回调的java方法

jmethodID onGetStringFromC = (*env)->GetStaticMethodID(env, clazz, "onGetStringFromC","(Ljava/lang/String;)V");

//回调

(*env)->CallStaticVoidMethod(env, clazz, onGetStringFromC, result);

//release

(*env)->DeleteLocalRef(env, result);

}

此外,当想要查找java方法对应的方法签名的时候,androidStudio可以很方便地用代码提示地方式自动生成方法签名:在方法签名参数位置先填上"",然后光标放过去,并按住alt+enter,就出现了提示:

f506ea55c9066b62cb923b717503b504.png

2.3 必要的释放

这里用到了释放函数:(*env)->DeleteLocalRef(env, jobject);,该函数是用来释放在c层创建的java对象的局部引用。什么时候该使用这个函数来释放在c层创建的jobject对象呢?异步回调形式的jni函数。

那么为什么同步返回的jni函数不需要呢?

实际上,在c层调用jni函数NewXXX()函数来创建一个对象的时候,会把这个对象放入到当前jni函数特有的一段内存区域中,称为本地引用根的集合,每当在c层调用NewXXX()函数创建java对象的时候,都会把创建好的对象的引用保存一份在这个本地引用根的集合中。

我认为这么做的原因是防止JVM在GC的时候把这个对象给干掉了,因为这个对象这个时候还在c层,没有返回给Java层,即java层是没有任何引用指向这个对象的。这种机制是为了保护这个在c层创建的java对象。

而当从当前的jni函数返回的时候(或者在c层创建的一个附着到JVM的线程结束了,即从JVM脱离了),就会把这个本地引用根的集合释放掉,返回到Java层的那些对象就可以接着被java层去引用,去处理,而没有返回java层的对象,在这个时候已经是GC unreachable了,就会被干掉了。

但是!!!,一个jin方法可能并没有那么简单的逻辑,就创建两个对象就返回给Java了,很多时候,jni方法进入到c层之后,会在c层开启新的线程,而新的线程中又会去通过NewXXX()函数创建其他的java对象,并在c层主动发起回调,将该对象和其他可能的数据传到java层。这个时候,线程中创建的所有的java对象只有在线程结束的时候才会释放掉本地引用根的集合,如果不手动释放,那么这些对象在返回到java层之后,使用之后,c层也不会再去使用,就造成了内存泄漏了。

因此,最好的方法就是:在调用NewXXX()创建了Java对象之后,除非这个对象马上通过当前的jni函数返回到java(c层主动回调java函数的不算,必须是当前jni方法),否则使用完之后要调用(*env)->DeleteLocalRef(env, jobject);来释放这个引用。

参考:https://www.ibm.com/support/knowledgecenter/SSYKE2_7.0.0/com.ibm.java.aix.70.doc/diag/understanding/jni_gc.html

我自己对这个参考文档做了一些翻译:

翻译参考文献:

JNI对象引用概览

关于GC如何找到JNI对象的引用的实现细节并没有在JNI文档中说明。但是,JNI文档的确指明了JNI对象所需要的一些可靠和可预见的表现。

本地和全局引用

本地引用被限制在创建他们的栈和线程中,并且在创建他们的栈帧返回的时候就会被自动删除。

全局引用允许原生代码去将一个本地引用升级成一种可以被任何附着到JVM的线程访问的全局引用。

全局引用和内存泄漏

全局引用不会自动被删除,所以开发者必须处理他们的内存管理。每一个全局引用都建立了一个GC root,并且让他的整个GCtree都可到达。因此,每一个被创建的GCroot必须被手动释放以防止内存泄漏。

内存泄漏最终导致OOM,这些错误很难处理,尤其是你没有实现JNI异常处理。

为了解决内存泄漏的问题,JNI提供了两个方法:

NewWeakGlobalRef

DeleteWeakGlobalRef

这些方法能用弱引用的方式来避免内存泄漏。

本地引用和内存泄漏

在大多数情况下,GC对那些不在栈帧范围中的本地引用的自动的垃圾回收已经足够适用。这种自动GC回收会在原生线程(或原生方法)返回到java或者从JVM中脱离的时候发生。但是,如果不满足这种条件的时候,就可能会发生本地引用的内存泄漏。例如:当原生方法并没有返回到java或者一个线程创建了本地引用但是却并没有从JVM中脱离。

考虑以下代码的情况,原生代码在一个循环中不断地创建新的本地引用:

while ( )

{

jobject myObj = (*env)->NewObject( env, clz, mid, NULL );

if ( NULL != myObj )

{

/* we know myObj is a valid local ref, so use it */

jclass myClazz = (*env)->GetObjectClass(env, myObj);

/* uses of myObj and myClazz, etc. but no new local refs */

/* Without the following calls, we would leak */

(*env)->DeleteLocalRef( env, myObj );

(*env)->DeleteLocalRef( env, myClazz );

}

} /* end while */

尽管myObj和myClazz变量在循环中每次都指向了新的对象,但是,用这些jni方法创建的每一个新的对象都在本地引用根的集合中被引用了。

这些引用必须被手动地移除,使用DeleteLocalRef方法。如果不调用这个方法,这些本地引用会一直保持着泄漏,直到这个方法返回到java或者线程从jvm中脱离。

JNI弱全局引用

弱全局引用是一种特殊的全局引用。他们可以在任何线程中被使用,并且可以在jni方法之间被调用,并且不会作为GC root。GC会在任何时候将一个没有强引用的对象回收掉。

你必须小心地使用弱全局引用。如果该若全局引用被垃圾回收了,那么这个引用就指向了null,一个null引用只能被少部分JNI函数调用。去检查一个弱全局引用是否已经别回收掉了,使用IsSameObject()来和NULL对比。

大多数JNI函数对弱全局引用的调用都是不安全的,即使你测试过这个弱引用是非空的。因为这个弱全局引用可能在检测过后或者在你调用过程中又被回收了。你应该在使用弱全局引用之前,将他升级成一个强引用。例如使用:NewLocalRef或者NewGlobalRef

弱全局引用的使用必须要调用DeleteWeakGlobalRef来释放。否则会导致缓慢的GC,最终还是会导致OOM。

JNI引用管理

There are a set of platform-independent rules for JNI reference management

These rules are:

JNI references are valid only in threads attached to a JVM.

A valid JNI local reference in native code must be obtained:

As a parameter to the native code

As the return value from calling a JNI function

A valid JNI global reference must be obtained from another valid JNI reference (global or local) by calling NewGlobalRef or NewWeakGlobalRef.

The null value reference is always valid, and can be used in place of any JNI reference (global or local).

JNI local references are valid only in the thread that creates them and remain valid only while their creating frame remains on the stack.

1 N2J转换

当原生代码在一个线程中调用java代码时,这个线程必须先附着到当前进程的JVM上。

每一个传递了对象应用的N2J(native to java)调用必须要通过jni方法获取有效的jobject对象才能传递到java,因此他们需要是有效的本地或者全局jni引用。任何从N2J方法返回的引用都是JNI本地引用。

2 J2N调用

JVM必须确保任何从java到native传递的对象和在native中创建的新的java对象都是GC可到达的(否则在下一次GC的时候就对象就被回收了)。要满足这个GC的要求,JVM分配了一块小段特殊的内存称为:本地引用根的集合。

本地引用根的集合在以下情况会被创建:

一个线程被附着到JVM上时。

每一个J2N转换发生。(即对象从java传到c层,要防止这个对象被JVM意外回收,因此要放在本地引用根的集合)

在原生代码中创建的新的对象都会被添加到这个J2N根集合中,除非你用PushLocalFrameJNI方法来创建一个新的本地栈。

默认的本地引用根的集合是足够大的,能够在每个J2N调用中容纳16个本地引用。

3 J2N返回

当从原生方法返回到java时,对应的JNI本地引用,即由这次J2N方法创建的本地引用根的集合会被自动释放。

如果JNI本地引用是某个对象的唯一的引用,那么这个对象在从J2N方法返回的时候就不再GC可到达并且会自动触发他的垃圾回收。这种机制简化了JNI开发者的内存管理。

<think>我们正在解决一个JNI函数无法解析的问题,具体错误是:JNI function unresolved Java_com_isa_navi_jni_hmi_HmiProxyJNI_get_1Global_1m_1data_1address 这个错误通常发生在JNI调用时,动态链接库无法找到对应的本地函数实现。以下是完整的解决步骤: ### 1. 检查函数签名是否完全匹配 函数名必须严格按照JNI规范生成:`Java_<包名>_<类名>_<方法名>`,其中: - 包名中的`.`替换为`_` - 类名方法名保持不变 - 特殊字符(如下划线)在方法名中会被转换为`_1` 根据函数名`Java_com_isa_navi_jni_hmi_HmiProxyJNI_get_1Global_1m_1data_1address`,我们可以推断: - 包名:`com.isa.navi.jni.hmi` - 类名:`HmiProxyJNI` - 方法名:`get_Global_m_data_address`(注意:在JNI中,下划线会被转义为`_1`,因此原方法名中的下划线被转换成了`_1`) 在C/C++中实现的函数必须完全匹配这个名称,包括大小写。 示例C++函数声明: ```cpp #include <jni.h> extern "C" JNIEXPORT jlong JNICALL Java_com_isa_navi_jni_hmi_HmiProxyJNI_get_1Global_1m_1data_1address(JNIEnv* env, jobject obj) { // 实现代码 return reinterpret_cast<jlong>(&global_m_data); // 假设返回一个全局变量的地址 } ``` ### 2. 检查动态库加载是否正确 在Java代码中,确保在调用本地方法前已经正确加载了包含该函数的动态库。 示例Java代码: ```java package com.isa.navi.jni.hmi; public class HmiProxyJNI { static { System.loadLibrary("hmi_proxy_jni"); // 加载名为 libhmi_proxy_jni.so 或 hmi_proxy_jni.dll 的库 } public native long get_Global_m_data_address(); // 本地方法声明 } ``` ### 3. 验证动态库是否包含目标函数 使用以下命令检查动态库中的符号: - **Linux/macOS**: ```bash nm -gD libhmi_proxy_jni.so | grep Java_com_isa_navi_jni_hmi_HmiProxyJNI_get_1Global_1m_1data_1address ``` 如果有输出,说明函数存在于库中。 - **Windows** (使用Visual Studio Developer Command Prompt): ```bash dumpbin /EXPORTS hmi_proxy_jni.dll | findstr Java_com_isa_navi_jni_hmi_HmiProxyJNI_get_1Global_1m_1data_1address ``` ### 4. 检查编译选项 确保编译动态库时: - 使用`extern "C"`防止C++名称修饰(name mangling) - 添加`JNIEXPORT``JNICALL`宏(这些宏确保函数在动态库中可见且使用正确的调用约定) ### 5. 排查路径问题 动态库必须位于Java库路径下,可通过以下方式设置: ```java System.setProperty("java.library.path", "/path/to/libs"); ``` 或者将动态库放在默认库搜索路径中(如Linux的`/usr/lib`,Windows的`PATH`环境变量包含的目录)。 ### 6. 检查函数签名冲突 如果同一个类中有多个同名本地方法(但参数不同),JNI函数名需要包含参数签名。但本例中函数名没有参数部分,说明是无参方法。如果有参数,则函数名应为: ``` Java_com_isa_navi_jni_hmi_HmiProxyJNI_get_1Global_1m_1data_1address__<参数签名> ``` 其中参数签名规则如下: - 无参数:不需要后缀 - 有参数:每个参数类型对应一个缩写(如`I`表示`int`,`J`表示`long`),并用下划线分隔 例如,如果原方法为`long get_Global_m_data_address(int mode)`,则函数名应为: ``` Java_com_isa_navi_jni_hmi_HmiProxyJNI_get_1Global_1m_1data_1address__I ``` ### 7. 重新生成JNI头文件(可选) 使用`javah`或`javac -h`生成头文件,确保函数名正确: ```bash javac -h . com/isa/navi/jni/hmi/HmiProxyJNI.java ``` 生成的头文件会包含正确的函数签名,直接复制到C++文件中使用。 ### 8. 异常处理 如果以上步骤都正确,但仍然无法解析,检查是否在JNI函数中抛出了异常而未处理,导致后续调用失败。 ### 示例修正后的代码结构 **Java端**: ```java package com.isa.navi.jni.hmi; public class HmiProxyJNI { static { System.loadLibrary("hmi_proxy_jni"); } public native long get_Global_m_data_address(); } ``` **C++端** (`hmi_proxy_jni.cpp`): ```cpp #include <jni.h> // 全局变量定义 int global_m_data = 42; extern "C" JNIEXPORT jlong JNICALL Java_com_isa_navi_jni_hmi_HmiProxyJNI_get_1Global_1m_1data_1address(JNIEnv* env, jobject obj) { return reinterpret_cast<jlong>(&global_m_data); } ``` ### 编译命令示例 - **Linux/macOS**: ```bash g++ -shared -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux hmi_proxy_jni.cpp -o libhmi_proxy_jni.so ``` - **Windows**: ```bash cl /I"%JAVA_HOME%\include" /I"%JAVA_HOME%\include\win32" /LD hmi_proxy_jni.cpp /Fehmi_proxy_jni.dll ``` ### 常见错误原因总结 1. **函数名拼写错误**:大小写、下划线转义错误(如`_1`缺失) 2. **动态库未加载**:`System.loadLibrary`失败(路径错误或库名不正确) 3. **符号不可见**:缺少`extern "C"`或`JNIEXPORT` 4. **动态库与JVM架构不匹配**:如64位JVM加载32位库 --- §§相关问题§§ 1. 如何解决JNI调用中的`UnsatisfiedLinkError`? 2. 在JNI中如何正确处理对象引用以防止内存泄漏? 3. JNI方法签名如何生成?</think>### 解决JNI函数无法解析的问题:`Java_com_isa_navi_jni_hmi_HmiProxyJNI_get_1Global_1m_1data_1address` 这个错误表明JVM在动态库中找不到对应的本地函数实现。以下是完整的排查解决方案: --- #### 1. **检查函数签名匹配性**(最常见原因) JNI函数名必须严格遵循命名规范:`Java_{包路径}_{类名}_{方法名}`,其中: - 包路径中的`.`替换为`_` - 方法名中的下划线`_`需转义为`_1` - 特殊字符需按JNI规范转义 根据你的函数名: ```cpp // 正确签名示例(对比检查) JNIEXPORT jlong JNICALL Java_com_isa_navi_jni_hmi_HmiProxyJNI_get_1Global_1m_1data_1address( JNIEnv* env, jobject obj ) { // 实现代码 } ``` **重点检查**: - 包名`com.isa.navi.jni.hmi`是否完整 - 类名`HmiProxyJNI`大小写是否一致 - 方法名中的每个`_`是否都转义为`_1` --- #### 2. **验证动态库加载状态** 在Java类中添加加载诊断: ```java public class HmiProxyJNI { static { try { System.loadLibrary("your_library_name"); // 替换实际库名 System.out.println("JNI库加载成功"); } catch (UnsatisfiedLinkError e) { System.err.println("JNI库加载失败: " + e.getMessage()); e.printStackTrace(); } } } ``` **常见问题**: - 库名不匹配(Windows需省略`.dll`,Linux需省略`lib``.so`) - 库文件不在`java.library.path`中 - 架构不匹配(如64位JVM加载32位库) --- #### 3. **检查本地库符号导出** 使用工具验证函数是否在库中: - **Linux/macOS**: ```bash nm -gD libyour_library.so | grep get_1Global_1m_1data_1address ``` - **Windows** (VS开发人员命令提示符): ```powershell dumpbin /EXPORTS your_library.dll | findstr get_1Global_1m_1data_1address ``` **若无输出**: - 确保编译时有`extern "C"`(防止C++名称修饰) - 添加`JNIEXPORT`宏(确保符号可见) --- #### 4. **完整解决方案示例** ```cpp #include <jni.h> // 全局变量(示例) int global_m_data = 42; // 严格匹配签名的JNI函数 extern "C" JNIEXPORT jlong JNICALL Java_com_isa_navi_jni_hmi_HmiProxyJNI_get_1Global_1m_1data_1address( JNIEnv* env, jobject obj ) { // 返回全局变量地址(转换为long) return reinterpret_cast<jlong>(&global_m_data); } ``` --- #### 5. **编译与部署要点 | 平台 | 编译命令示例 | |------------|-----------------------------------------------------------------------------| | **Linux** | `g++ -shared -fPIC -I${JAVA_HOME}/include -o libhmi_jni.so HmiProxyJNI.cpp` | | **Windows**| `cl /I"%JAVA_HOME%\include" /I"%JAVA_HOME%\include\win32" /LD HmiProxyJNI.cpp` | | **Android**| 在`Android.mk`中添加:`LOCAL_CFLAGS += -I$(JNI_H_INCLUDE)` | **关键参数**: - `-shared`/`/LD`:生成动态库 - `-I`:包含JNI头文件路径 - `-fPIC`:位置无关代码(Linux必需) --- #### 6. **进阶调试技巧 1. **生成JNI头文件**(验证签名): ```bash javac -h . com/isa/navi/jni/hmi/HmiProxyJNI.java ``` 检查生成的`.h`文件中的函数签名 2. **启用JNI检查**(捕获更多错误): ```bash java -Xcheck:jni YourMainClass ``` 3. **日志跟踪**: ```cpp #include <android/log.h> #define LOG_TAG "JNI_DEBUG" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) JNIEXPORT jlong JNICALL ... { LOGD("get_1Global_1m_1data_1address called"); return ...; } ``` --- ### 常见错误原因总结 | 原因 | 解决方案 | |---------------------|----------------------------| | 函数签名拼写错误 | 使用`javac -h`生成头文件对比 | | 缺少`extern "C"` | 用`extern "C"`包裹函数声明 | | 未导出符号 | 添加`JNIEXPORT`宏 | | 动态库未加载 | 检查`System.loadLibrary()` | | 架构不匹配 | 确保JVM库同为32/64位 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值